Skip to content
Jack Gerrits edited this page Apr 25, 2023 · 22 revisions

⚠️ This page is not final and not decided or agreed upon. It is in draft form for if we decide to move forward with this strategy.

TL;DR

  • For recoverable errors, return a VW::result<T> from the function
    • This should be used in 99% of cases
    • Always decorate with [[nodiscard]] to avoid accidentally not checking
  • For unrecoverable errors, use VW_THROW (can be disabled with VW_NOEXCEPT)
    • In our codebase, this means out of memory errors and programming bugs

Error Handling in VW

Starting in VW 10, there is a new error handling system. The system is very similar to how Rust handles errors. It distinguishes between handling recoverable and unrecoverable errors.

Recoverable errors

Recoverable errors are communicated through the VW::result<T> type which is a type alias for tl::expected<T, VW::error>. This type is used to return either a value or an error. For functions with no return value, VW::result<void> is used.

The VW::error type contains an error code enum and an error message. The error code enum is used to determine the type of error that occurred. The error message is a string that contains more information about the error.

Therefore, for all functions which can fail VW::result<T> should be used. All functions which return VW::result<T> should be marked with [[nodiscard]] to ensure that the error is checked.

Unrecoverable errors

For unrecoverable errors (a programming bug or other error that cannot be recovered from) VW_THROW should be used, which will by default throw an exception.

Failure to allocate memory is considered an unrecoverable error and std::bad_alloc will be thrown.

Therefore, a consumer of VowpalWabbit could decide to catch these very few exceptions and handle them in a way that is appropriate for their application. But in general, these represent programming bugs and should be fixed or unrecoverable errors.

Example:

void do_thing()
{
  VW_THROW(std::runtime_error("do_thing() is broken."));
}

For a noexcept environment VW_NOEXCEPT can be enabled which will turn the throw into printing a diagnostic and calling std::abort instead of throwing the exception.

Using VW::result<T>

To return the expected value, simply return the value:

[[nodiscard]] VW::result<int> foo() { return 5; }

To return success in a void function:

[[nodiscard]] VW::result<void> foo() { return {}; }

To return a generic error, use VW::make_error:

[[nodiscard]] VW::result<int> foo() { return VW::make_error("Function failed."); }

To return a specific error code:

[[nodiscard]] VW::result<int> foo() { return VW::make_error(VW::error_code::IO, "File operation failed."); }

To check if the result is an error or value, use VW::result<T>::has_value(). It is also convertible to a bool. Because VW::result<T> follows value semantics there are many ways to properly handle and use the type.

auto result = foo();
if (result) { std::cout << "Success! Value is: " << *result; }
else { return result; }

// or
ASSIGN_OR_RETURN(result, foo());
std::cout << "Success! Value is: " << *result;

// or
auto result = foo().map([](int value) { std::cout << "Success! Value is: " << value; });
if (!result) { return result; }

Light errors

There is a compile feature called LIGHT_ERRORS which, when enabled, will make the error type only contain an error code enum. This is useful for when you don't need the error message. This is disabled by default. When enabled, rich and dynamic error messages are not available.

This turns VW::error into a trivially copyable type which may be useful for performance. However, it is unclear if this actually helps. Please benchmark and compare if you are interested in using this feature.

Testing

test_common exposes matchers to make writing tests for VW::result easy.

#include "vw/test_common/matchers.h"

[[nodiscard]] VW::result<void> foo();
[[nodiscard]] VW::result<int> bar();

TEST(testsuite, test) {
  // EXPECT_OK is a matcher that checks if the result contains a value (even void)
  EXPECT_OK(foo());

  // This is equivalent to:
  EXPECT_THAT(foo(), HasValue());

  // HasValueAndHolds is a matcher that checks if the result contains a value and that the value matches the given value
  EXPECT_THAT(bar(), HasValueAndHolds(13));

  // HasErrorCode is a matcher that checks an operation failed with a specific error code
  EXPECT_THAT(foo(), HasErrorCode(VW::error_code::GENERIC));
}

Error domains

VW::error is actually a typedef of VW::details::error_impl<VW::error_code>. Therefore, VW::error_code is the error domain, and in VW this is the default domain. Most of the time in library code this is what should be used and for vw_core's API this should be the error domain exposed via the interface.

However, for libraries such as the explore library it is helpful to have a custom error domain. This aids in separating the list of all possible error codes that have to be reasoned over for a given error.

The types used in the explore lib are:

using error = ::VW::details::error_impl<VW::explore::explore_error_code>;

template <typename T>
using result = ::VW::expected<T, VW::explore::error>;

VW::make_error will automatically use the correct error type if any error code enum is passed. If only a string is passed to VW::make_error then it will return the default VW::error.

If crossing the boundary between two error domains then the types need to be mapped to each other. This can be done conveniently with map_error.

Handling failure during construction

We follow the same model as LLVM for handling errors during construction.

Constructors can never fail. If construction can fail, the class constructors should be private and the class should expose a static create function which returns a result containing the constructed object. Any object constructed in this way most be moveable. If the type cannot be moved then the create function can return a unique_ptr instead.

Two-phase initialization is not allowed in the codebase.

Example

struct Foo
{
  VW::result<Foo> create(int value)
  {
    if (value < 0)
    {
      return VW::make_error("Invalid value");
    }

    return Foo{value};
  }

private:
  Foo(int value) : _value(value) {}

  int _value;
}
Clone this wiki locally