Custom error codes

This section illustrates how you can hook into the std::error_code system from the Standard Library in order to work with your own set of error codes. As is usually the case in C++, doing this is straightforward but requires typing boilerplate to tell the C++ STL about your custom error type. This is not part of Outcome library, but we still provide this short guide here, because how to do this is not well documented [1].

Suppose you want to report all reasons for failure in converting a std::string to a non-negative int. The list is:

  • EmptyString – the input string is empty,
  • IllegalChar – input contains characters that are not digits,
  • TooLong – input represents a number, but this number would not fit into a variable of type int.

#include <iostream>
#include <string>        // for string printing
#include <system_error>  // bring in std::error_code et al

// This is the custom error code enum
enum class ConversionErrc
{
  Success     = 0, // 0 should not represent an error
  EmptyString = 1,
  IllegalChar = 2,
  TooLong     = 3,
};

namespace std
{
  // Tell the C++ 11 STL metaprogramming that enum ConversionErrc
  // is registered with the standard error code system
  template <> struct is_error_code_enum<ConversionErrc> : std::true_type
  {
  };
}

namespace detail
{
  // Define a custom error code category derived from std::error_category
  class ConversionErrc_category : public std::error_category
  {
  public:
    // Return a short descriptive name for the category
    virtual const char *name() const noexcept override final { return "ConversionError"; }
    // Return what each enum means in text
    virtual std::string message(int c) const override final
    {
      switch (static_cast<ConversionErrc>(c))
      {
      case ConversionErrc::Success:
        return "conversion successful";
      case ConversionErrc::EmptyString:
        return "converting empty string";
      case ConversionErrc::IllegalChar:
        return "got non-digit chatr when converting to a number";
      case ConversionErrc::TooLong:
        return "the number would not fit into memory";
      default:
        return "unknown";
      }
    }
    // OPTIONAL: Allow generic error conditions to be compared to me
    virtual std::error_condition default_error_condition(int c) const noexcept override final
    {
      switch (static_cast<ConversionErrc>(c))
      {
      case ConversionErrc::EmptyString:
        return make_error_condition(std::errc::invalid_argument);
      case ConversionErrc::IllegalChar:
        return make_error_condition(std::errc::invalid_argument);
      case ConversionErrc::TooLong:
        return make_error_condition(std::errc::result_out_of_range);
      default:
        // I have no mapping for this code
        return std::error_condition(c, *this);
      }
    }
  };
}

// Define the linkage for this function to be used by external code.
// This would be the usual __declspec(dllexport) or __declspec(dllimport)
// if we were in a Windows DLL etc. But for this example use a global
// instance but with inline linkage so multiple definitions do not collide.
#define THIS_MODULE_API_DECL extern inline

// Declare a global function returning a static instance of the custom category
THIS_MODULE_API_DECL const detail::ConversionErrc_category &ConversionErrc_category()
{
  static detail::ConversionErrc_category c;
  return c;
}


// Overload the global make_error_code() free function with our
// custom enum. It will be found via ADL by the compiler if needed.
inline std::error_code make_error_code(ConversionErrc e)
{
  return {static_cast<int>(e), ConversionErrc_category()};
}

int main(void)
{
  // Note that we can now supply ConversionErrc directly to error_code
  std::error_code ec = ConversionErrc::IllegalChar;

  std::cout << "ConversionErrc::IllegalChar is printed by std::error_code as "
    << ec << " with explanatory message " << ec.message() << std::endl;

  // We can compare ConversionErrc containing error codes to generic conditions
  std::cout << "ec is equivalent to std::errc::invalid_argument = "
    << (ec == std::errc::invalid_argument) << std::endl;
  std::cout << "ec is equivalent to std::errc::result_out_of_range = "
    << (ec == std::errc::result_out_of_range) << std::endl;
  return 0;
}
View this code on Github

This might look like a lot of extra boilerplate over simply using your custom error code enum directly, but look at the advantages:

  1. Any code which can speak std::error_code can now work with errors from your code, AND without being recompiled.
  2. std::system_error can now wrap your custom error codes seamlessly, allowing your custom error code to be converted into a C++ exception and back out again without losing information.
  3. std::error_code knows how to print itself, and will print your custom error code without extra work from you. As usually you’d need to define a print routine for any custom error code you’d write anyway, there is actually very little extra boilerplate here.
  4. If you implement the default_error_condition() override, you can allow code exclusively written to understand std::errc alone to examine your custom error code domain for equivalence to the standard error conditions, AND without being recompiled.

This documentation recommends that when you define your custom enum for representing error_codes, you should always make sure that value 0 never represents an actual error: it should either represent a success or should not be provided at all. If you only intend to use your enum inside result<> or outcome<> you can just start your enumerations from 1. If you intend to also return std::error_code directly from functions, you should probably define value 0 as success, so that you are able to inform about function’s success by returning MyEnum::Success. This is because error_code’s contextual conversion to bool (which some people use to check if there was an error or not) only checks for the numeric value of the error code (without looking at error domain (category)).

[1]: The only documentation I’m aware of is the quite old guide by Chris Kohlhoff, founder of ASIO and the Networking TS: