Exception-free error handling
In NOIR project, C++ exception is not prohibited strictly, but its usage is limited to really exceptional cases, not for branching code. Instead, noir::Result<T, E>
is used for error handling, which is analogous to std::result::Result<T, E>
in Rust language.
template<typename T, typename E = noir::Error>
class Result;
noir::Result<T, E>
is derived from the result type of Boost.Outcome, so its member methods can be called.
noir::Error
is the default error type for noir::Result
. It has similar interface to std::error_code
, but doesn’t inherit from it. To use std::error_code
, error value (integer) and compile-time error message should be defined in prior, but noir::Error
supports instantiation from user-provided runtime string for convenience.
How to use
Result<int> get_value() {
return 1; // return value
return success(1); // return value wrapped with success() // return success() with no arguments for Result<void>
// return success(); return Error("unexpected error");
return Error::format("unexpected value: {}", -1); // internally, fmt::format is used
return Error(error_code, error_category); // like std::error_code
return failure(...); // constructs an error with arguments
}
Other approaches
- Why isn’t
std::expected<T, E>
(C++23) used for this purpose?
std::expected<int, Error> get_value();if (auto ok = get_value(); !ok) {
// std::expected doesn't support construction from an error,
// it needs to wrap an error with std::unexpected every time
// to propagate an error. // not supported
// return ok.error(); return std::unexpected(ok.error());
}
- Why isn’t
boost::outcome_v2::result<T, E>
used directly, butnoir::Result
inherits from it?
boost::asio::awiatable<Result<int>> awaitable_func();// compile error
boost::asio::co_spawn(io_context, awaitable_func(), boost::asio::detached);// boost::asio::awaitable<R> requires R is default constructible,
// but boost::outcome_v2::result doesn't have a default constructor.
// A default constructor is added to `noir::Result` to make it work
// with C++20 coroutines.
Comparison to other languages
- NOIR (C++)
auto get_value() -> Result<int> {}auto ok = get_value();
if (!ok) {
return ok.error();
}
auto value = ok.value();
- Rust
fn get_value() -> Result<i32> {}let value = get_value()?;
- Go
func get_value() (int, error)value, err := get_value()
if err != nil {
return err
}