Error Handling with expected (C++23)
std::expected<T, E> is a return type that carries either a success value or a typed error — and nothing else. It makes failure visible in a function's signature, lets you handle errors without exceptions, and chains fallible steps with monadic helpers like and_then and transform . By the end you'll return and consume expected values cleanly, and know when to pick it over exceptions or error codes.
Learn Error Handling with expected (C++23) in our free C++ course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of ordering food at a counter. An exception is a fire alarm: when something goes wrong, an alarm blares and everyone up the chain has to react, even if they had nothing to do with your order. A bare error code is a little number on a screen you might forget to glance at, while your tray sits there separately — easy to grab the food and ignore the warning. std::expected is the tray itself: it comes back holding either your meal or a slip explaining what went wrong, never both, and you cannot eat without first looking at what is actually on the tray. The outcome — success or a clear reason for failure — is right there in your hands.
1. A Value or an Error
A function returning std::expected<T, E> hands back one of two things: a value of type T on success, or an error of type E on failure. You return the value directly on the happy path; on the error path you wrap your error with std::unexpected(e) . Callers tell the cases apart with has_value() (or the implicit bool conversion), then read value() on success or error() on failure. Crucially, the failure is now part of the return type — the signature itself warns every caller.
When an error simply means "use a default", value_or collapses the whole check into one expression — it returns the value on success or your fallback on failure, with no branching:
Your turn. Complete doubleIfNonNeg so it reports an error for negative input using std::unexpected :
2. Chaining with and_then, transform & or_else
Real code rarely fails in one place — it threads a value through several fallible steps. Instead of nesting if -checks, std::expected offers monadic helpers. and_then runs a function that itself returns an expected , but only when a value is present; an error short-circuits straight through. transform maps the success value with a plain function ( T -> U ) and rewraps the result. or_else runs only on the error path, so you can recover or translate the error. The pipeline then reads top to bottom, and the first failure skips the rest.
These lines should call a fallible function and print either the value or the error. Put them in the right order:
Open main (D), call the fallible function (A), print the value when present (B), otherwise print the error (C), then return 0; (E) and close the brace (F).
3. expected vs Exceptions vs Error Codes
Three ways to report failure, three trade-offs. An exception propagates invisibly up the stack until something catches it — great for rare, non-local errors, but the failure is absent from the signature and has a cost on the throw path. A bare error code is explicit and cheap, but the actual result usually lands in a separate out-parameter you can read whether or not you checked the code. std::expected bundles the value and the typed error into one return: failure is visible in the signature, the value and the error are mutually exclusive, and you can't read the value while ignoring the failure.
Rule of thumb: reach for expected when failure is a normal, recoverable outcome; keep exceptions for the genuinely exceptional.
E can be anything. A std::string message is the simplest choice, but for real systems prefer a small enum class of error codes, or a struct carrying a code and context. Because the error type is part of expected<T, E> , callers can switch on it exhaustively instead of string-matching, and you keep the type safety the whole way up.
Not on C++23 yet? tl::expected is a header-only library with the same interface, including the monadic helpers, that works back to C++11. Write your code in the expected style now, and when you move to C++23 you switch from tl::expected / tl::unexpected to std::expected / std::unexpected with little more than an include and namespace change.
Predict the result before revealing the answer.
-1 — the expected holds an error, so value_or returns the fallback.
No — transform only runs on the value path. An error short-circuits, so r still holds the error "x" .
3. How do you build the error for expected<int, string> ?
return std::unexpected(std::string("bad")); — unexpected wraps the value as the error alternative.
Write two fallible steps returning expected<int, string> , chain them with and_then and transform , and watch an early error short-circuit the whole pipeline straight to value_or .
Practice quiz
What does std::expected<T, E> represent?
- A value of type T OR an error of type E, but never both
- A value that is always present
- A pointer to T or to E
- A pair containing both a T and an E
Answer: A value of type T OR an error of type E, but never both. std::expected<T, E> holds either a success value of type T or an error of type E — exactly one of the two at a time.
Which header and standard introduced std::expected?
- <variant>, C++20
- <system_error>, C++11
- <expected>, C++23
- <optional>, C++17
Answer: <expected>, C++23. std::expected lives in the <expected> header and was added in C++23.
How do you check whether a std::expected holds a value rather than an error?
- exp.is_ok()
- exp.has_value() (or the implicit bool conversion)
- exp.valid()
- exp.success()
Answer: exp.has_value() (or the implicit bool conversion). has_value() returns true on success; the type also converts to bool, so you can write if (exp) to test it.
How do you construct an error result for std::expected<int, std::string>?
- throw std::string("bad input");
- return -1;
- return nullptr;
- return std::unexpected(std::string("bad input"));
Answer: return std::unexpected(std::string("bad input"));. std::unexpected(e) wraps a value as the error alternative, which then implicitly converts to the expected's error state.
What does exp.error() return?
- The stored error object (only valid when has_value() is false)
- A bool
- Always the default error
- The success value
Answer: The stored error object (only valid when has_value() is false). error() accesses the stored error of type E; it is only meaningful when the expected is in the error state.
What does and_then do on a std::expected?
- Replaces the error with a default
- Chains a function that itself returns an expected, only when a value is present
- Always runs, ignoring errors
- Throws if there is an error
Answer: Chains a function that itself returns an expected, only when a value is present. and_then calls the given function (which returns another expected) only on the value; an error short-circuits straight through.
How does transform differ from and_then?
- transform ignores the value
- transform only works on errors
- They are identical
- transform's function returns a plain value (T -> U), which is rewrapped into expected
Answer: transform's function returns a plain value (T -> U), which is rewrapped into expected. transform maps the success value with a function returning a plain U and rewraps it; and_then expects a function that already returns an expected.
Which monadic helper lets you handle or recover from the error case?
- and_then
- transform
- or_else
- value_or
Answer: or_else. or_else runs its function only when the expected holds an error, letting you recover or produce a different expected.
Compared with exceptions, a key property of std::expected is that:
- It always allocates on the heap
- Errors are part of the function's return type and visible in its signature
- It cannot carry an error message
- It can only report integer error codes
Answer: Errors are part of the function's return type and visible in its signature. expected makes the possible failure explicit in the return type, so callers must acknowledge it — unlike an exception that travels invisibly up the stack.
Before C++23, which library provided a drop-in std::expected-like type?
- tl::expected
- std::experimental::optional
- absl::Status only
- boost::any
Answer: tl::expected. tl::expected is a widely used header-only implementation that mirrors std::expected for codebases not yet on C++23.