Monadic error handling in C++ and the ? operator
May 30, 2025
Errors as values are common in C and a great alternative to exceptions in C++. In C++23, std::expected
is going to provide a monadic interface for errors as values. Now if only we had Rust’s ?
operator or Haskell’s do
notation…
tl;dr
Using GNU extensions Statement expressions, usable in both gcc and clang, however not implemented in msvc , one can emulate the ?
operator from Rust:
#define TRY(x) ({ auto y = x; if (!y) return y; y.value(); })
std::expected<int, Error> safe_divide(int a, int b) {
if (b == 0) {
return std::unexpected(ErrorCode::DIVISION_BY_ZERO);
}
return a / b;
}
std::expected<int, Error> algo(int a, int b) {
int result = TRY(safe_divide(a, b)); // returns on error
return std::log(result);
}
int main() {
if (auto result = algo(1, 0, 3); result) {
printf("Result: %d", result.value());
} else {
printf("Error: %d", result.error());
}
}
Error as values
We start with a basic example of exceptions:
int division(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
int main() {
try {
int result = division(1, 0);
printf("Result: %d", result);
} catch (std::runtime_error) {
printf("Division by zero");
}
}
If one does not want to use exceptions, one could instead write:
bool division(int a, int b, int* result) {
if (b == 0) {
return false;
}
*result = a / b;
return true;
}
int main() {
int result;
if (division(1, 0, &result)) {
printf("Result: %d", result);
} else {
printf("Division by zero");
}
}
I like this better than exceptions. The function signature cleary communicates the failure states. And you either handle the error or you don’t, but the program flow is clear.
What I don’t like about this is the mix of status as return value and result as output argument. The function has two results, that is the status code and the calculation result. They should be returned together.
A simple Result container
If I was to quickly sketch something like this using C++17:
template<typename TValue, typename TError>
class Result {
public:
Result(TValue&& value) : value_(std::forward<TValue>(value)) {}
Result(TError&& error) : error_(std::forward<TError>(error)) {}
operator bool() const { return has_value(); }
bool has_value() const { return value_.has_value(); }
const TValue& value() const { return value_.value(); }
const TError& error() const { return error_.value(); }
private:
std::optional<TValue> value_;
std::optional<TError> error_;
};
And then applying it to the example from above:
enum class Error {
DivisionByZero,
};
Result<int, Error> division(int a, int b) {
if (b == 0) {
return Error::DivisionByZero;
}
return a / b;
}
int main() {
if (auto result = division(1, 0); result) {
printf("Result: %d", result.value());
} else {
printf("Error: %d", result.error());
}
}
Now that’s readable code! If I had one complaint, it would be that the function signature should be explicit about which errors specifically it can return.
The current approach would simply accumulate error codes from different contexts in enum class Error
.
Enter std::expected
Instead of rolling my own Result
class, I can wait until C++23 to just use std::expected
See cppreference , or until then, use a compatible implementation, like tl::expected
See TartanLama on Github. For C++ 11, 14 and 17. :
enum class Error {
DivisionByZero,
};
tl::expected<int, Error> division(int a, int b) {
if (b == 0) {
return tl::unexpected(Error::DivisionByZero);
}
return a / b;
}
int main() {
if (auto result = division(1, 0); result) {
printf("Result: %d", result.value());
} else {
printf("Error: %d", result.error());
}
}
The code stays pretty much the same, with the exception of having to wrap the error in a tl::unexpected
. Which makes sense, since otherwise cases where value and error type are equal don’t work.
Monadic error handling
std::expected
allows chaining of results, similar to std::optional
:
enum class Error {
DivisionByZero,
LogOfZero,
};
tl::expected<int, Error> division(int a, int b) {
if (b == 0) {
return tl::unexpected(Error::DivisionByZero);
}
return a / b;
}
tl::expected<int, Error> safe_log(int a) {
if (a == 0) {
return tl::unexpected(Error::LogOfZero);
}
return std::log(a);
}
int main() {
auto result = division(0, 1)
.and_then(safe_log)
.map([](int x) { return x * 2; });
if (result) {
printf("Result: %d", result.value());
} else {
switch (result.error()) {
case Error::DivisionByZero:
printf("Division by zero");
break;
case Error::LogOfZero:
printf("Log of zero");
break;
}
}
}
The methods expected::and_then
and expected::or_else
can be chained together to create complex flows of logic.
Personally, I don’t like this. This forces a certain code style which I find unpleasant to work with. The result likely are small functions and lambdas.
Rather, this would be nice:
tl::expected<int, Error> work() {
int div = division(0, 1)?; // returns on error
int res = log(div)?;
return x * 2;
}
The ?
operator in C++
C++ does not allow to return from within a statement. We can’t use return
inside an expression.
Or… can we? gcc has a non-standard extension called statement expressions. It allows us to write a whole code block as part of an expression:
int v = ({
if (true) {
return 0;
}
123;
});
So v
would be 123, that is if the function wouldn’t return before assigning the value to v
.
This can be used to create a macro which checks the result of a tl::expected
:
#define TRY(x) ({ \
auto y = (x); \
if (!y) { \
return y; \
} \
y.value(); \
})
tl::expected<int, Error> algo(int a, int b) {
int div = TRY(division(0, 1));
int ret = TRY(safe_log(div));
return x * 2;
}
int main() {
tl::expected<int, Error> result = algo(1, 0);
}
By the way, this is what the ladybird browser See Github repository uses throughout their codebase. They surely weren’t the first, but it’s a battle tested technique over there.