C++, End to End
Error Detection and Handling
Not every error is the same kind of error, and C++ gives you several different tools because "how should this fail" genuinely has different right answers depending on the situation. This chapter is about telling those situations apart, not about picking one true way to handle every error (a full deep dive on exceptions specifically comes later in this course).
Programmer errors vs. runtime failures
Worth separating up front: a programmer error is a violated precondition, calling a function with an argument it explicitly documented as invalid, indexing past the end of an array, dereferencing a null pointer. These are bugs. A runtime failure is something that can legitimately happen even in correct code: a file doesn't exist, a network call times out, user input doesn't parse as a number. Bugs should crash loudly and immediately in development. Runtime failures need to be handled gracefully, because they're expected, not exceptional in the sense of being rare.
assert for programmer errors
assert(condition) (from <cassert>) checks a condition and aborts the program immediately if it's false, but only in debug builds. Defining NDEBUG (which most build systems do automatically for release builds) strips asserts out entirely, so they cost nothing in production and should never be used for anything the program depends on at runtime.
The && "message" trick above is a common idiom: a non-null string pointer is always truthy, so ANDing it into the condition doesn't change the logic, but the message shows up in the assertion failure output and makes debugging faster.
static_assert for compile-time checks
static_assert(condition, "message") checks a condition at compile time and fails the build (not the running program) if it's false. Since it's compile-time, the condition has to be something the compiler can evaluate without running your program, sizes of types, template parameters, constexpr values.
Two strategies for runtime failures: return values and exceptions
For failures that need to be handled, not just asserted away in debug builds, C++ code broadly falls into two camps:
- Signal failure through the return type. Error codes,
std::optional<T>when there's no meaningful value to return,std::expected<T, E>(C++23) when you also want to explain why it failed. The caller has to check. - Throw an exception. The failure propagates up the call stack automatically until something catches it. The caller doesn't have to check at every level, but forgetting to catch anywhere means the program terminates.
Neither is universally correct. A rough, honest heuristic: use return-based signaling for failures that are routine and expected as part of normal control flow (a lookup that might not find anything, parsing input that might be malformed). Reach for exceptions when the failure is genuinely exceptional, when propagating it up several call frames without every intermediate function having to know or care is exactly the behavior you want, or when a constructor fails (constructors have no return value to signal failure with, so exceptions are close to the only clean option).
std::optional: values that might not exist
std::optional<T> (from <optional>) wraps a value that may or may not be present, without needing a sentinel value like -1 or nullptr that could be confused with a legitimate result.
Dereference an optional with * or -> once you've confirmed it holds a value (via the if check, or .has_value()). Dereferencing an empty optional is undefined behavior, exactly the kind of programmer error assert exists to catch during development.
[[nodiscard]]
This attribute marks a function's return value as something the caller shouldn't silently throw away. It's a small thing, but it directly prevents a real class of bugs: writing error-signaling functions that return a status and then forgetting to check it.
Try it yourself: replace a sentinel value with std::optional›
This function uses -1 to mean "not found", which silently breaks if -1 ever becomes a valid index in some future version of the code. Rewrite it to return std::optional<size_t> instead.