Skip to content

MihaiStreames/anyhow-cpp

Repository files navigation

anyhow-cpp ¯\_(°ペ)_/¯

Header-only C++20 result type with propagating stacktraced errors, inspired by Rust's anyhow.

C++ Release CI License

#include <anyhow.hpp>

Install

FetchContent

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

git submodule add https://github.com/MihaiStreames/anyhow-cpp.git external/anyhow-cpp
add_subdirectory(external/anyhow-cpp)
target_link_libraries(your-target PRIVATE anyhow::anyhow)

Usage

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;
}

Early return

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 {};
}

Propagation

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)

Context

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]

Downcasting

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';
    }
}

Inspection

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";

Chaining

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);
    });

Scope guard

ScopeGuard runs a callable on scope exit. Call release() to cancel.

auto guard = anyhow::ScopeGuard{[&] { cleanup(); }};

Frame buffer depth

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>

vs anyhow

See docs/vs-rust.md for a feature-by-feature comparison with side-by-side Rust and C++ examples.

Acknowledgments

Thanks to Belmu -- the original error-handling design in Noble Engine is what this started from :)

License

MIT. See LICENSE.

Made with ❤️

Sponsor this project

Contributors