folly/result/docs/rich_error_code.md
Modern C++ applications must handle exceptions (because nearly everything throws), but they may also want to be more disciplined with internal error flows -- for those, returning "value or error code" is a common pattern.
Rich error codes integrate with folly::result & rich errors. Compared to
``, they provide similar-but-better functionality.
Codes are value types that fits in a uint64_t.
A rich error type may export one or more code types (e.g. general &
specific, or OS & application-internal), with efficient access via the
rich_error_base::retrieve_code() virtual call.
Users can query any error container supporting folly::get_exception via
get_rich_error_code<Code>(container).
Rich code access is fast & ergonomic when used with folly/result tools.
rich_error_code.h?If you already use rich errors -- for the automatic source locations, for the
ergonomic error-provenance epitaphs, for the speedy mostly-no-RTTI
performance, or for any of its other benefits -- then the reason to adopt
rich_error_code is that it is built-in! Key integrations:
Rich error formatting automatically displays all the codes from an error.
...coded_rich_error.h and errc_rich_error.h cover all the common uses.
But, the underlying protocols support custom composition & inheritance, too.
get_rich_error_code<Code>(...) works on all standard error-containers
(including anything that speaks folly::get_exception). It is extra-fast
for result / error_or_stopped, and works for std::exception_ptr, etc.
Fast type-unerasure -- checking if a type-erased error has a Code is much
faster than RTTI (<5ns typically). First, we get_rich_error(container),
which is specifically optimized for *result containers. Once you have a
rich_error_base*, code retrieval is reliably RTTI-free.
If you do not yet use rich errors, you may want to scroll down to "Prior art" for a quick comparison with other error code designs.
Define a code type (typically, an enum class) and specialize rich_error_code:
#include <folly/result/rich_error_code.h>
enum class FruitCode { BAD, UNRIPE, READY, OVERRIPE };
template <>
struct folly::rich_error_code<FruitCode> {
// Generate with: python3 -c 'import random;print(random.randint(0, 2**64 - 1))'
// DO NOT CHANGE once committed - this is part of your ABI!
static constexpr uint64_t uuid = 16014278773182690925ULL;
};
// Optional / encouraged. Use an exhaustive switch from `folly/lang/Switch.h`.
struct fmt::formatter<FruitCode> { /* ... */ };
Now, you can use coded_rich_error<FruitCode> for typical error-handling. Power
users:
nestable_coded_rich_error.h if coded_rich_error isn't enough.rich_error_code.h.Since all errors with the same code are logically interchangeable, you can get significant efficienty wins from using immortal error instances in hot code -- these are almost as cheap as regular integer codes!
using namespace folly::string_literals; // for `_litv` suffix
static constexpr badFruit = immortal_rich_error<
coded_rich_error<FruitCode>,
FruitCode::BAD,
"Rotten, moldy, or damaged"_litv>.ptr();
result<double> fruitToCalories(Fruit f) {
if (!f.isGood()) {
return error_or_stopped{badFruit};
}
// ...
};
The beauty of immortal errors is that they quack just like their dynamic
counterparts. You can still epitaph to add context, convert them to a
dynamic std::exception_ptr, throw them, etc.
But, if you need a dynamic error, you can change the above code like so,
without breaking any contracts. The dominant cost will be ~60ns to allocate
(and later free) an extra std::exception_ptr on the heap.
if (f.isMoldy()) {
return error_or_stopped{make_coded_rich_error(
FruitCode::BAD, "Moldy {}: {}", f.name(), diagnoseMold(f))};
}
Get std::optional<Code> from any error container using get_rich_error_code:
struct Human {
// ...
result<> eatLunch(Lunch l) {
// ...
{
auto res = fruitToCalories(l.fruit_);
if (auto code = get_rich_error_code<FruitCode>(res)) {
// Real code might use `FOLLY_EXHAUSTIVE_SWITCH`
if (code == FruitCode::BAD) {
makeYuckFace();
} else { /* ... */ }
} else {
energy_ += co_await or_unwind(res);
}
}
}
}
This API is particularly fast with folly/result containers, but will works on
anything with folly::get_exception support.
For a high-level comparison of result with std::expected, boost::outcome,
and other coded-error alternatives, see design_notes.md.
The closest thing to standard error-code handling is ``. Sadly,
it has many defects. For details, see P0824, or a summary
in Niall Douglas's status_code docs).
For end-users, perhaps the greatest downside is the high conceptual complexity.
If in doubt, here are two long blog posts on defining an application-specific
error category using std:
1,
2.
For other prior art, read the front matter to Niall Douglas's P1028
status_code. Standardization of this design was
abandoned by the author as of August 2024. The code is
available on Github, and also available
as boost/outcome/experimental.
Herb Sutter's P0709 is also worth reading for a thorough exploration of the problem space.
Instead of repeating what has been said before, let's cover what folly/result
does differently from std and status_code:
Our code types (enum class FruitError) are self-contained -- you don't
need to know a "category" or "domain" to use a code. For value-type T to
be usable, it must fit in uint64_t and specialize rich_error_code<T>.
folly/result supports type erasure of errors via error_or_stopped.
Going from error_or_stopped to rich_error_base* is deliberately fast.
We piggyback on this to avoid introducing any additional notion of
"category" or "domain". Each rich error class is its own domain. This
lets us synthesize extremely fast lookup of Code from rich_error_base*.
In P1028, std::errc / generic_code is special, in that cross-domain
comparisons become possible by uniformly converting application codes to
errc. We don't bring this complexity into the core design. Either make
your codes comparable, or provide multiple codes per error, and compare the
compatible ones.
As a result, the rich error code design works much better in folly/result:
folly/result ecosystem.status_code.