Header-only C++20 result type with propagating stacktraced errors, inspired by Rust's anyhow.
#include <anyhow.hpp>include(FetchContent)
FetchContent_Declare(
anyhow-cpp
GIT_REPOSITORY https://github.com/MihaiStreames/anyhow-cpp.git
GIT_TAG v0.1.0
)
FetchContent_MakeAvailable(anyhow-cpp)
target_link_libraries(your-target PRIVATE anyhow::anyhow)git submodule add https://github.com/MihaiStreames/anyhow-cpp.git external/anyhow-cppadd_subdirectory(external/anyhow-cpp)
target_link_libraries(your-target PRIVATE anyhow::anyhow)Include everything at once with anyhow.hpp, or pull in individual headers as needed.
| Header | Provides |
|---|---|
anyhow.hpp |
Includes all headers below + Result<T> alias |
expected.hpp |
Expected<T>, Expected<void> |
failure.hpp |
Failure, Unexpected, fail(), fail_with(), chain(), root_cause() |
macros.hpp |
ANYHOW_TRY, ANYHOW_TRY_ASSIGN, ANYHOW_TRY_CATCH, ANYHOW_BAIL, ANYHOW_ENSURE |
scope_guard.hpp |
ScopeGuard |
Note
Define ANYHOW_SHORT_MACROS before including macros.hpp to enable the short aliases TRY, TRY_ASSIGN, TRY_CATCH, BAIL, ENSURE.
anyhow::Result<T> is an alias for Expected<T>, matching Rust's naming convention.
Use Expected<T> (or Result<T>) as the return type of any fallible function. Return anyhow::fail(message, domain) on failure, or return the value directly on success.
anyhow::Expected<int> parse(std::string_view s) {
if (s.empty()) {
return anyhow::fail("empty input", "parse");
}
return 42;
}ANYHOW_BAIL returns a failure immediately. ANYHOW_ENSURE does the same when a condition is false.
anyhow::Expected<int> parse(std::string_view s) {
ANYHOW_ENSURE(!s.empty(), "empty input", "parse");
// ...
return 42;
}
anyhow::Expected<void> validate(int n) {
if (n < 0) ANYHOW_BAIL("negative value");
return {};
}Use ANYHOW_TRY_ASSIGN to unwrap a value or propagate the failure up. Each macro captures the current call site via std::source_location and pushes it onto the frame buffer, building a trace as the error travels up the stack.
anyhow::Expected<std::string> process(std::string_view s) {
int n = 0;
ANYHOW_TRY_ASSIGN(n, parse(s));
ANYHOW_TRY(validate(n));
return std::to_string(n);
}auto result = process("");
if (result.failed()) {
auto& fail = result.failure();
std::cout << "error [" << fail.error.domain << "]: " << fail.error.message << '\n';
for (size_t i = 0; i < fail.count; i++) {
std::cout << " at " << fail.frames[i].function << " (" << fail.frames[i].file << ':' << fail.frames[i].line << ")\n";
}
}error [parse]: empty input
at anyhow::Expected<int> parse(std::string_view) (src/main.cpp:8)
at anyhow::Expected<std::string> process(std::string_view) (src/main.cpp:17)Wrap failures with human-readable context as they propagate up the call stack. context takes a string eagerly; with_context takes a callable and only evaluates it on failure.
anyhow::Expected<Config> load(std::string_view path) {
return parse_file(path)
.context("failed to parse config")
.with_context([&] { return "loading config from " + std::string(path); });
}On failure, Failure::fmt() renders context outermost-first followed by the root error:
loading config from /etc/app/config.toml
failed to parse config
unexpected token at line 42 [parse]Attach a typed payload with fail_with() and recover it with Failure::downcast<T>(), which returns a pointer or null.
anyhow::Expected<void> open(std::string_view path) {
return anyhow::fail_with(errno, "open failed", "io");
}
auto result = open("/missing");
if (result.failed()) {
if (auto* code = result.failure().downcast<int>()) {
std::cout << "errno: " << *code << '\n';
}
}chain() iterates context layers outermost-first then the root error message. root_cause() returns the root ErrorInfo directly.
for (auto cause : failure.chain()) {
std::cerr << cause << '\n';
}
const auto& root = failure.root_cause();
std::cerr << root.message << " [" << root.domain << "]\n";map, and_then, and value_or are available for functional chaining on results.
auto result = parse("21")
.map([](int v) { return v * 2; })
.and_then([](int v) -> anyhow::Expected<std::string> {
return std::to_string(v);
});ScopeGuard runs a callable on scope exit. Call release() to cancel.
auto guard = anyhow::ScopeGuard{[&] { cleanup(); }};Override at compile time (default: 16). When full, oldest frames are evicted. Must be defined before any anyhow include.
#define ANYHOW_MAX_FRAMES 32
#include <anyhow.hpp>See docs/vs-rust.md for a feature-by-feature comparison with side-by-side Rust and C++ examples.
Thanks to Belmu -- the original error-handling design in Noble Engine is what this started from :)
MIT. See LICENSE.