Error handling is a topic I avoided for so long while working on my game engine. Most often I just return a std::optional from function calls to indicate failure and print more details to the log. This simple approach worked surprisingly well, however, it has a few drawbacks.

  1. The error message also gets printed when the calling function is perfectly able to deal with such a failure. This leads to unwanted error messages in the log file that might confuse the user.
  2. If the calling function is not able to handle the error you manually have to propagate it up the call stack.
  3. Mapping the error messages in the log to failed function calls can be tricky if there are multiple error messages. Especially in the context of scripting, it becomes awkward to say “Something has failed, please look at the log to figure out what.”

When it comes to error handling C++ is a mess. The standard library itself uses three different ways of dealing with errors:

  1. errno
  2. error_code
  3. exceptions

It encourages the use of exceptions, however, they are very frowned upon in the community as they violate the zero-overhead principle of C++1 and there is a lot of discussion on how to make them more efficient2 3 4 5 6. For me it is especially bad as my main target platform is WebAssembly which does support exceptions at all yet (yes, there is a proposal and some browsers have already implemented it but it cannot be polyfilled and it will take some time until it is widely available)7. Emscripten is able to implement exception handling but this comes with very high overhead. Thus, using exceptions for error handling is no option for me.

I also looked at the error handling strategies of other more modern languages such as Go, Swift and Rust and I especially liked the way Rust deals with errors. It has a result type Result<T,E> that either holds the return value of the function T or an error E. The type itself is similar to a std::variant<T, E> in C++ and there is even a proposal for something very similar in C++: std::expected<T, E> (see also the great talk from Andrei Alexandrescu on this topic). This is almost what I am looking for, however, there are a few details I do not quite like. If you access the value via expected<T, E>::value() and the type contains an error it throws the error which is not possible when compiling with exceptions disabled. The * and -> operators also allow you to access the underlying values (like in std::optional<T>) and they produce undefined behavior if the type contains an error. This is good, except for the undefined behavior part of course: I would like to have a little bit more safety there. In debug mode, there are probably assertions that ensure that the result actually contains a value but if the error does not occur during development and the code just assumes the result holds a valid value we are in trouble. What I would like is to assure that you have to check whether the result contains a value before using it. The Error class in llvm does something similar. It has a checked flag that is set as soon as the bool operator is called. In their implementation the flag is checked in the destructor and abort() is called when no one checked the error. This is not exactly what I want: if the result was never checked but the value was also never accessed I do not care. I only want to detect whether the value is accessed before it was checked.

As there does not seem to be a solution out there that does exactly what I want, I wrote my own Result class:

template <typename T, typename E = Error>
class Result {
public:
  Result(T value) : has_value_(true) {
    new (&data_) T(std::move(value));
  }

  Result(E error) : has_value_(false) {
    new (&data_) E(std::move(error));
  }

  ~Result() {
    if (has_value_) {
      data_as_value()->~T();
    } else {
      data_as_error()->~E();
    }
  }

  [[nodiscard]] bool has_value() const {
#ifndef NDEBUG
    has_been_checked_ = true;
#endif
    return has_value_;
  }

  [[nodiscard]] operator bool() const {
    return has_value();
  }

  const E& error() const {
    assert(!has_value_);
    assert(has_been_checked_);
    return *data_as_error();
  }

  T* operator->() {
    assert(has_value_);
    assert(has_been_checked_);
    return data_as_value();
  }
  const T* operator->() const {
    assert(has_value_);
    assert(has_been_checked_);
    return data_as_value();
  }

  T& operator*() {
    assert(has_value_);
    assert(has_been_checked_);
    return *data_as_value();
  }
  const T& operator*() const {
    assert(has_value_);
    assert(has_been_checked_);
    return *data_as_value();
  }

private:
  std::aligned_union_t<0, T, E> data_;
  bool has_value_;
#ifndef NDEBUG
  mutable bool has_been_checked_ = false;
#endif
  static_assert(!std::is_constructible_v<T, E>);

  T* data_as_value() {
    return reinterpret_cast<T*>(&data_);
  }
  const T* data_as_value() const {
    return reinterpret_cast<const T*>(&data_);
  }

  E* data_as_error() {
    return reinterpret_cast<E*>(&data_);
  }
  const E* data_as_error() const {
    return reinterpret_cast<const E*>(&data_);
  }
};

As you can see the has_been_checked_ flag is only there in debug mode, so it should not cause any runtime overhead. Using the value before checking will cause the assertion to fail (regardless of whether the result contains a T or an E);

Result<int> integer = ...;
use(*integer);

It will help to assure the result is used more like this:

Result<int> integer = ...;
if (value) {
  use(*integer);
} else {
  // Whatever
}

However, it is not perfect as it will not detect the invalid use in the following scenario:

Result<int> integer = ...;
if (integer) {
  use(*integer);
} else {
  use(*integer);
}

or

Result<int> integer = ...;
const bool checked = integer;
use(*integer);

But I do not see any way to detect this without static code analysis.

Currently, I use only a very simple error class that the template parameter E defaults to that only contains an error message:

struct Error {
  template <typename... Args>
  Error(std::string_view message, Args&&... args)
      : message(std::format(message, std::forward<Args>(args)...)) {}

  std::string message;
};

And finally here is a small example to see it in action:

Result<std::string> get_file_content(std::string_view filename) {
  std::ifstream file(filename);
  if (!file.is_open()) {
    return Error("File not found: {}", filename);
  }

  file.seekg(0, std::ios::end);
  std::vector<char> buffer(file.tellg());
  file.seekg(0, std::ios::beg);
  file.read(buffer.data(), buffer.size());

  return std::string(buffer.data(), buffer.data() + buffer.size());
}

Result<unsigned int> get_file_hash(std::string_view filename) {
  const auto content = get_file_content(filename);
  if (!content) {
    return content.error();
  }

  unsigned int hash = 0;
  for (char c : *content) {
    hash += c;
  }

  return hash;
}

int main(int, char* argv[]) {
  const auto hash = get_file_hash(argv[1]);
  if (!hash) {
    std::cerr << hash.error().message << std::endl;
  } else {
    std::cout << *hash << std::endl;
  }
}

In contrast do exceptions the propagation of the error to the calling function is written explicitly. In general, I think is a good thing but I would like a nicer syntax for it like the ? operator in rust or the proposed try statement for C++. One could define a macro for this:

#define CHECK_RESULT(expression)           \
  if (auto result = expression; !result) { \
    return result.error();                 \
  }

and use it like this:

Result<unsigned int> get_file_hash(std::string_view filename) {
  const auto content = get_file_content(filename);
  CHECK_RESULT(content);

  unsigned int hash = 0;
  for (char c : *content) {
    hash += c;
  }

  return hash;
}

However, I am not quite sure whether I prefer it over the explicit if.

Overall I am not completely satisfied with the result as I would really like a coherent way for error handling in C++ with proper language support but that probably will not happen in the near future (most likely never). However,