Skip to content

Commit f63a4e1

Browse files
committed
Content complete
1 parent f7a4ec5 commit f63a4e1

File tree

1 file changed

+18
-11
lines changed

1 file changed

+18
-11
lines changed

_posts/2025-10-06-cpp-std-expected.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,11 @@ std::expected<std::string, std::string> read_qr_code(std::vector<uint8_t> const&
8585
}
8686
```
8787

88-
Note that in this example, both the success and error type are strings, but they could be any type. In a lot of cases, it might still make sense to use an enum or a custom error type for the error case to make it more structured. However, by using `std::expected`, we already are able to add a lot more context to the function without cluttering the code. This already is a big improvement over returning, but there is more.
88+
Note that in this example, both the success and error type are strings, but they could be any type. In a lot of cases, it might still make sense to use an enum or a custom error type for the error case to make it more structured. However, by using `std::expected`, we already are able to add a lot more context to the function without cluttering the code. This already is a big improvement over returning, but there is more. `std::expected` also provides a set of powerful combinators for composing operations that may fail, allowing for more elegant error handling.
8989

9090
### Monadic chaining with `and_then`
9191

92-
93-
Monadic chaining lets you compose a sequence of operations that may fail, without deeply nested `if`/`else`. With `std::expected`, you chain:
92+
One of the benefits of `std::expected` is that it allows for [monadic chaining](https://en.wikipedia.org/wiki/Monad_(functional_programming)), A technique widely used in functional programming. Monadic chaining lets you compose a sequence of operations that may fail, without deeply nested `if`/`else`. With `std::expected`, you chain:
9493
- `and_then` when the next step itself may fail and returns another `expected`.
9594
- `transform` when the next step cannot fail and just maps the value.
9695
- `or_else` to act on or recover from an error.
@@ -109,7 +108,6 @@ Below is a continuation of the QR example, showing a pipeline that:
109108
#include <expected>
110109
#include <string>
111110
#include <vector>
112-
#include <cctype>
113111

114112
struct Uri {
115113
std::string scheme;
@@ -143,7 +141,7 @@ parse_uri(std::string const& s) {
143141
return std::unexpected("Not an http(s) URI");
144142
}
145143

146-
// very naïve split: scheme://host/path
144+
// very naive split: scheme://host/path
147145
auto pos = s.find("://");
148146
auto rest = s.substr(pos + 3);
149147
auto slash = rest.find('/');
@@ -180,9 +178,18 @@ extract_qr_host(std::vector<uint8_t> const& image) {
180178
}
181179
```
182180
183-
Notes and gotchas:
184-
- Keep the error type `E` consistent across `and_then`/`or_else` steps. If you must change it, use `transform_error`.
185-
- Use `and_then` only with functions that return `expected<..., E>`. Use `transform` for pure mappings returning plain values.
186-
- The chain short-circuits on the first error, returning that error downstream.
187-
- If a step can throw, those exceptions still propagate unless caught and converted to `std::unexpected`.
188-
- Prefer passing by `const&` in chain steps to avoid copies. If you need to move, adapt the function to accept by value and `std::move` internally.
181+
As we see, the resulting code in `extract_qr_host` is linear and easy to read. Each step is clearly defined, and error handling is centralized without deeply nested conditionals. The use of `and_then`, `transform`, and `or_else` makes the intent of each operation explicit.
182+
183+
There are some pitfalls and good practices to keep in mind when using monadic chaining with `std::expected`:
184+
185+
* Keep the error type `E` consistent across `and_then`/`or_else` steps. If you must change it, use `transform_error`.
186+
* Use `and_then` only with functions that return `expected<..., E>`. Use `transform` for pure mappings returning plain values.
187+
* The chain short-circuits on the first error, returning that error downstream. So having a catch-all `or_else` at the end is a good practice.
188+
* If a step can throw, those exceptions still propagate unless caught and converted to `std::unexpected`.
189+
* Prefer passing by `const&` in chain steps to avoid copies or use move semantics. However the guaranteed copy elision in C++17 and later often makes this less of a concern.
190+
191+
With these practices in mind, `std::expected` and its combinators can greatly enhance the clarity and robustness of error handling in your C++ code.
192+
193+
## final thoughts
194+
195+
With the arrival of `std::expected` in C++23 there is another powerful tool in C++ to allow more expressive code in a functional programming style. This can make applications that do a lot of data processing and have many recoverable failure paths much cleaner and easier to maintain. While it does not replace exceptions for unrecoverable errors, it nicely complements them by providing a structured way to handle expected errors. And the beauty of it is, that it still works seamlessly with existing C++ code and libraries - So no need to go all in and change it everywhare. So give it a try in your next C++ project and see how it can improve your error handling!

0 commit comments

Comments
 (0)