11 / 12
Guide Book Β· Chapter 11

Error handling

Exceptions and the guarantees they come with, `std::optional` and `std::expected`, assertions, and when each is the right tool.

C++ gives you several error mechanisms and an opinion about none of them. Choosing well is most of the work.

Exceptions

cpp
#include <iostream>
#include <stdexcept>
#include <string>

int parsePort(const std::string& text) {
  if (text.empty()) throw std::invalid_argument("port is empty");

  int value = 0;
  try {
    value = std::stoi(text);
  } catch (const std::invalid_argument&) {
    throw std::invalid_argument("not a number: " + text);
  }

  if (value < 1 || value > 65535) {
    throw std::out_of_range("port out of range: " + std::to_string(value));
  }
  return value;
}

int main() {
  for (const std::string input : {"8080", "abc", "99999", ""}) {
    try {
      std::cout << "port " << parsePort(input) << "\n";
    } catch (const std::out_of_range& e) {
      std::cout << "range error: " << e.what() << "\n";
    } catch (const std::invalid_argument& e) {
      std::cout << "bad input: " << e.what() << "\n";
    }
  }
}

Catch by const reference. Catching by value slices a derived exception down to the base type and loses the real message.

The standard hierarchy

cpp
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>

int main() {
  std::vector<int> v{1, 2, 3};

  try {
    [[maybe_unused]] int x = v.at(99);
  } catch (const std::exception& e) {
    std::cout << "std::exception caught: " << e.what() << "\n";
  }

  try {
    throw std::logic_error("a bug in the program");
  } catch (const std::runtime_error&) {
    std::cout << "not reached β€” logic_error is not a runtime_error\n";
  } catch (const std::logic_error& e) {
    std::cout << "logic_error: " << e.what() << "\n";
  }
}
text
std::exception
β”œβ”€β”€ std::logic_error          a bug: the caller broke a precondition
β”‚   β”œβ”€β”€ std::invalid_argument
β”‚   β”œβ”€β”€ std::domain_error
β”‚   β”œβ”€β”€ std::length_error
β”‚   └── std::out_of_range
β”œβ”€β”€ std::runtime_error        something outside the program failed
β”‚   β”œβ”€β”€ std::range_error
β”‚   β”œβ”€β”€ std::overflow_error
β”‚   └── std::system_error
└── std::bad_alloc, std::bad_cast, std::bad_optional_access, …

Exception safety guarantees

cpp
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>

class Settings {
 public:
  // Strong guarantee: either it fully succeeds, or nothing changed.
  void replaceAll(std::vector<std::string> incoming) {
    validate(incoming);          // may throw β€” before any mutation
    values_ = std::move(incoming);
  }

  // Basic guarantee: still valid afterwards, but partially updated.
  void appendEach(const std::vector<std::string>& incoming) {
    for (const auto& v : incoming) {
      if (v.empty()) throw std::invalid_argument("empty value");
      values_.push_back(v);
    }
  }

  std::size_t size() const noexcept { return values_.size(); }   // no-throw

 private:
  static void validate(const std::vector<std::string>& items) {
    for (const auto& item : items) {
      if (item.empty()) throw std::invalid_argument("empty value");
    }
  }

  std::vector<std::string> values_;
};

int main() {
  Settings s;

  try {
    s.replaceAll({"a", "", "c"});
  } catch (const std::exception& e) {
    std::cout << "replaceAll threw: " << e.what() << ", size still " << s.size() << "\n";
  }

  try {
    s.appendEach({"a", "", "c"});
  } catch (const std::exception& e) {
    std::cout << "appendEach threw: " << e.what() << ", size now " << s.size() << "\n";
  }
}
GuaranteeMeans
no-throw (noexcept)never throws β€” required for destructors, swap, moves
strongthe operation either completes or leaves everything unchanged
basicno leaks, everything still valid, but state may have changed
noneavoid

std::optional

For "there might be no value", where absence is normal rather than exceptional.

cpp
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> parseInt(const std::string& text) {
  try {
    return std::stoi(text);
  } catch (...) {
    return std::nullopt;
  }
}

std::optional<std::string> findUser(const std::vector<std::string>& users, char initial) {
  for (const auto& u : users) {
    if (!u.empty() && u.front() == initial) return u;
  }
  return std::nullopt;
}

int main() {
  for (const std::string input : {"42", "abc"}) {
    if (auto n = parseInt(input)) {
      std::cout << input << " β†’ " << *n << "\n";
    } else {
      std::cout << input << " β†’ no value\n";
    }
  }

  const std::vector<std::string> users{"Ada", "Grace"};
  std::cout << findUser(users, 'A').value_or("(none)") << "\n";
  std::cout << findUser(users, 'Z').value_or("(none)") << "\n";

  auto missing = findUser(users, 'Z');
  std::cout << "has_value: " << std::boolalpha << missing.has_value() << "\n";
}
cpp
#include <iostream>
#include <optional>

int main() {
  std::optional<int> empty;
  std::cout << empty.value(); // error! bad_optional_access
}

*opt and opt->x do not check β€” they are undefined behaviour when empty. .value() throws. Test first, or use value_or.

std::expected

C++23's answer for "failed, and here is why", without exceptions.

cpp
#include <iostream>
#include <expected>
#include <string>

enum class ParseError { Empty, NotANumber, OutOfRange };

std::string describe(ParseError e) {
  switch (e) {
    case ParseError::Empty: return "empty input";
    case ParseError::NotANumber: return "not a number";
    case ParseError::OutOfRange: return "out of range";
  }
  return "unknown";
}

std::expected<int, ParseError> parsePort(const std::string& text) {
  if (text.empty()) return std::unexpected(ParseError::Empty);

  int value = 0;
  try {
    value = std::stoi(text);
  } catch (...) {
    return std::unexpected(ParseError::NotANumber);
  }

  if (value < 1 || value > 65535) return std::unexpected(ParseError::OutOfRange);
  return value;
}

int main() {
  for (const std::string input : {"8080", "abc", "99999", ""}) {
    auto result = parsePort(input);
    if (result) {
      std::cout << "[" << input << "] port " << *result << "\n";
    } else {
      std::cout << "[" << input << "] failed: " << describe(result.error()) << "\n";
    }
  }

  std::cout << parsePort("abc").value_or(-1) << "\n";
}

The failure is in the return type, so the caller cannot ignore it by accident β€” the win that exceptions give up.

Choosing

SituationReach for
A precondition was violated β€” the caller has a bugthrow std::logic_error (or assert)
Something outside the program failedthrow std::runtime_error
Absence is a normal outcomestd::optional
Failure is normal and the reason mattersstd::expected
An invariant the program depends onassert in debug builds
Unrecoverablestd::terminate / std::abort

Assertions and contracts

cpp
#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm>

int median(std::vector<int> values) {
  assert(!values.empty() && "median of an empty range is undefined");
  std::sort(values.begin(), values.end());
  return values[values.size() / 2];
}

int main() {
  std::cout << median({5, 1, 3}) << "\n";

  // static_assert is checked at compile time and costs nothing at run time.
  static_assert(sizeof(int) >= 4, "this code assumes a 32-bit int");
  std::cout << "static_assert passed at compile time\n";
}

assert is compiled out when NDEBUG is defined, which is the default for release builds. It documents a programmer-error invariant β€” never a condition that could occur from valid input.

noexcept

cpp
#include <iostream>
#include <vector>
#include <utility>

struct Fast {
  Fast() = default;
  Fast(Fast&&) noexcept = default;
  Fast& operator=(Fast&&) noexcept = default;
  Fast(const Fast&) = default;
};

int main() {
  std::cout << std::boolalpha;
  std::cout << "move is noexcept: " << noexcept(Fast{std::declval<Fast>()}) << "\n";
  std::cout << "a noexcept function that throws calls std::terminate\n";
  std::cout << "so mark it only where you can genuinely guarantee it\n";
}

⚠️ noexcept is a promise enforced by std::terminate, not by the type system. Mark destructors, swaps and move operations noexcept β€” the library checks for it and takes faster paths. Do not scatter it hopefully across functions that call things which might throw.

Reference: Exceptions, std::optional and std::expected on cppreference.

main.cpp
Loading editor…
No output yet
Press Run or βŒ˜β†΅ to compile and run your code.

Search

Jump to a lesson or a past run