Exception-free error handling

Conrad
Haderech_DEV
Published in
2 min readApr 26, 2022

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, but noir::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
}

--

--