C++23: A New Way of Error Handling with std::expected
C++23 extends the interface of std::optional and gets the new data type std::expected for error handling.

Before I dive into the extended monadic interface of std::optional in C++23, I want to introduce this C++17 type.
std::optional
std::optional is quite comfortable for calculations such as database queries that may have a result. This vocabulary type requires the header <optional>.
The various constructors and the convenience function std::make_optional let you define an optional object opt with or without a value. opt.emplace will construct the contained value in-place and opt.reset will destroy the container value. You can explicitly ask a std::optional container if it has a value, or you can check it in a logical expression. opt.value returns the value, and opt.value_or returns the value or a default value. If opt has no contained value, the call opt.value will throw a std::bad_optional_access exception.
Here is a short example using std::optional.
// optional.cpp #include <optional> #include <iostream> #include <vector> std::optional<int> getFirst(const std::vector<int>& vec){ if ( !vec.empty() ) return std::optional<int>(vec[0]); else return std::optional<int>(); } int main() { std::cout << '\n'; std::vector<int> myVec{1, 2, 3}; std::vector<int> myEmptyVec; auto myInt= getFirst(myVec); if (myInt){ std::cout << "*myInt: " << *myInt << '\n'; std::cout << "myInt.value(): " << myInt.value() << '\n'; std::cout << "myInt.value_or(2017):" << myInt.value_or(2017) << '\n'; } std::cout << '\n'; auto myEmptyInt= getFirst(myEmptyVec); if (!myEmptyInt){ std::cout << "myEmptyInt.value_or(2017):" << myEmptyInt.value_or(2017) << '\n'; } std::cout << '\n'; }
I use std::optional in the function getFirst. getFirst returns the first element if it exists. You get a std::optional object if not. The main function has two vectors. Both invoke getFirst and return a std::optional object. In the case of myInt, the object has a value; in the case of myEmptyInt, the object has no value. The program displays the value of myInt and myEmptyInt. myInt.value_or(2017) returns the value, but myEmptyInt.value_or(2017) returns the default value.
Here is the output of the program.

The Monadic Extension of std::optional
In C++23, std::optional is extended with monadic operations opt.and_then, opt.transform, and opt.or_else.
opt.and_thenreturns the result of the given function call if it exists or an emptystd::optional.opt.transformreturns astd::optionalcontaining its transformed value or an emptystd::optional.opt.or_elsereturns thestd::optionalif it contains a value or the result of the given function otherwise.
These monadic operations enable the composition of operations on std::optional:
// optionalMonadic.cpp #include <iostream> #include <optional> #include <vector> #include <string> std::optional<int> getInt(std::string arg) { try { return {std::stoi(arg)}; } catch (...) { return { }; } } int main() { std::cout << '\n'; std::vector<std::optional<std::string>> strings = {"66", "foo", "-5"}; for (auto s: strings) { auto res = s.and_then(getInt) .transform( [](int n) { return n + 100;}) .transform( [](int n) { return std::to_string(n); }) .or_else([] { return std::optional{std::string("Error") }; }); std::cout << *res << ' '; } std::cout << '\n'; }
The range-based for-loop iterates through the std::vector<std::optional<std::string>>. First, the function getInt converts each element to an integer, adds 100, converts it back to a string, and finally displays it. If the initial conversion to int fails, the string Error is returned and displayed.

std::expected already supports the monadic interface.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
std::expected
std::expected<T, E> provides a way to store either of two values. An instance of std::expected always holds a value: either the expected value of type T, or the unexpected value of type E. This vocabulary type requires the header <expected>. Thanks to std::expected, you can implement functions that either return a value or an error. The stored value is allocated directly within the storage occupied by the expected object. No dynamic memory allocation takes place.
std::expected has a similar interface, such as std::optional. In contrast to std::optional, std::exptected can return an error message.
The various constructors let you define an expected object exp with an expected value. exp.emplace will construct the contained value in-place. You can explicitly ask a std::expected container if it has a value, or you can check it in a logical expression. exp.value returns the expected value, and exp.value_or returns the expected value or a default value. If exp has an unexpected value, the call exp.value will throw a std::bad_expected_access exception.
std::unexpected represents the unexpected value stored in std::expected.
// expected.cpp #include <iostream> #include <expected> #include <vector> #include <string> std::expected<int, std::string> getInt(std::string arg) { try { return std::stoi(arg); } catch (...) { return std::unexpected{std::string(arg + ": Error")}; } } int main() { std::cout << '\n'; std::vector<std::string> strings = {"66", "foo", "-5"}; for (auto s: strings) { // (1) auto res = getInt(s); if (res) { std::cout << res.value() << ' '; // (3) } else { std::cout << res.error() << ' '; // (4) } } std::cout << '\n'; for (auto s: strings) { // (2) auto res = getInt(s); std::cout << res.value_or(2023) << ' '; // (5) } std::cout << '\n'; }
The function getInt converts each string to an integer and returns a std::expected<int, std::string>. int represents the expected, and std::string the unexpected value. The two range-based for-loops (lines 1 and 2) iterate through the std::vector<std::string>. In the first range-based for-loop (line 1), the expected (line 3) or the unexpected value (line 4) is displayed. In the second range-based for-loop (line 2), the expected or the default value 2023 (line 5) is displayed.

std::expected supports monadic operations for convenient function composition: exp.and_then, exp.transform, exp.or_else, and exp.transform_error.
- e
xp.and_thenreturns the result of the given function call if it exists or an emptystd::expected. exp.transformreturns astd::expectedcontaining its transformed value or an emptystd::exptected.exp.or_elsereturns thestd::expected if it contains a value or the result of the given function otherwise.exp.transform_errorreturns the std::expectedif it contains an expected value. Otherwise, it returns astd::expectedthat contains a transformed unexpected value.
The following program is based on the previous program optionalMonadic.cpp. Essentially, the type std::optional is replaced with std::expected.
// expectedMonadic.cpp #include <iostream> #include <expected> #include <vector> #include <string> std::expected<int, std::string> getInt(std::string arg) { try { return std::stoi(arg); } catch (...) { return std::unexpected{std::string(arg + ": Error")}; } } int main() { std::cout << '\n'; std::vector<std::string> strings = {"66", "foo", "-5"}; for (auto s: strings) { auto res = getInt(s) .transform( [](int n) { return n + 100; }) .transform( [](int n) { return std::to_string(n); }); std::cout << *res << ' '; } std::cout << '\n'; }
The range-based for-loop iterates through the std::vector<std::string>. First, the function getInt converts each string to an integer, adds 100, converts it back to a string, and finally displays it. If the initial conversion to int fails, the string arg + ": Error" is returned and displayed.

What’s Next?
The four associative containers std::flat_map, std::flat_multimap, std::flat_set, and std::flat_multiset in C++23 are a drop-in replacement for the ordered associative containers std::map, std::multimap, std::set, and std::multiset. We have them for one reason in C++23: performance.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, Honey Sukesan, bruce_lee_wayne, Silviu Ardelean, schnapper79, Seeker, and Sundareswaran Senthilvel.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
| My special thanks to Embarcadero | ![]() |
| My special thanks to PVS-Studio | ![]() |
| My special thanks to Tipi.build | ![]() |
| My special thanks to Take Up Code | ![]() |
| My special thanks to SHAVEDYAKS | ![]() |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org














