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.

What You'll Learn

💡 Real-World Analogy

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.

🧩 Reorder Challenge

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.

📊 Comparing the three approaches

Approach

Failure visible in signature?

Carries a typed reason?

Best for

exceptions

no

yes (the exception)

rare, non-local, hard-to-recover errors

error code

partly (return int)

weakly (a number)

low-level / C-style boundaries

expected

yes

yes (type E)

recoverable, expected failures

Rule of thumb: reach for expected when failure is a normal, recoverable outcome; keep exceptions for the genuinely exceptional.

🔎 Deep Dive: a richer error type, and pre-C++23 code

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.

Pro Tips

🧠 Quick Recall

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.

Common Errors (and the fix)

📋 Quick Reference

Task

Code

Declare

expected<int, string> f();

Return success

return 42;

Return error

return unexpected(e);

Test

if (r) / r.has_value()

Read value / error

r.value() / r.error()

Fallback

r.value_or(def)

Chain

r.and_then(f).transform(g)

Recover

r.or_else(h)

Frequently Asked Questions

Mini-Challenge: A Tiny Calculator Pipeline

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 .

🎉 Lesson Complete

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.

Continue this course

Related lessons