Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
AccessModifierOffset: -2

AlignAfterOpenBracket: BlockIndent
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignConsecutiveMacros: Consecutive
AlignConsecutiveAssignments: Consecutive
AlignConsecutiveDeclarations: Consecutive
AlignEscapedNewlines: Right
AlignOperands: false
AlignTrailingComments: false
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ jobs:
with:
persist-credentials: false

- name: Format check
if: matrix.os == 'ubuntu-latest'
run: find src tests -name '*.hpp' -o -name '*.cpp' | xargs clang-format --dry-run --Werror

- name: Configure
env:
CXX_STD: "20"
Expand Down
41 changes: 10 additions & 31 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,17 @@

All notable changes to this project.

## [0.1.1] - 2026-06-01
## [0.1.0] - 2026-06-16

### Added

- Docstrings on public types, members, and macros
- `Failure::context` vector for wrapping human-readable messages
- `Failure::push_context()` mutator for appending context messages
- `Failure::fmt()` renders context outermost-first followed by the root error; `operator<<` wraps it
- `Expected<T>::context(msg)` and `Expected<T>::with_context(fn)` for lazy context attachment (both `T` and `void` specializations)
- GTest suite: 31 tests across `Expected<T/void>`, context, fmt, `ScopeGuard`, and `ANYHOW_TRY*` macros

### Changed

- Dropped redundant `[[nodiscard]]` from `map`, `and_then`, `fail` (return type already carries it)
- `Error` renamed to `ErrorInfo`, freeing `Error` for the future type-erased error type (Phase 5)
- Member ordering standardized across all headers: constants -> fields -> constructors -> mutators -> accessors -> methods
- `Expected<T>` value constructors are now implicit, so `return value;` works directly from a fallible function (the error path stays explicit through `Unexpected`)
- Removed `Failure::message()` / `Failure::domain()` accessors; read `failure().error.message` / `failure().error.domain` directly (`Failure` is a transparent struct)

### Fixed

- `ScopeGuard` move-assign (`operator=(ScopeGuard&&)`) deleted to prevent accidental reassignment
- `i++` to `++i` in `Failure::push` ring-buffer loop

## [0.1.0] - 2026-05-31

### Added

- `Expected<T>` result type with `Unexpected` wrapper for explicit error construction
- `Failure` with ring-buffer frame storage, configurable via `ANYHOW_MAX_FRAMES`
- `Frame` capture via `std::source_location`
- `Expected<T>` / `Result<T>` result type with `Unexpected` for explicit error construction
- `Failure` with ring-buffer frame trace, configurable via `ANYHOW_MAX_FRAMES`
- `fail(msg, domain)` and `fail_with(payload, msg, domain)` for typed payloads
- `Failure::downcast<T>()`, `Failure::is<T>()`, `Failure::chain()`, `Failure::root_cause()`
- `Expected<T>::context(msg)` / `with_context(fn)` for wrapping errors with human-readable layers
- `ANYHOW_TRY`, `ANYHOW_TRY_ASSIGN`, `ANYHOW_TRY_CATCH` for error propagation with call-site capture
- `ANYHOW_BAIL`, `ANYHOW_ENSURE` for early return
- `ANYHOW_SHORT_MACROS` opt-in aliases: `TRY`, `TRY_ASSIGN`, `TRY_CATCH`, `BAIL`, `ENSURE`
- `ScopeGuard` with move support and `release()`
- `ANYHOW_TRY`, `ANYHOW_TRY_CATCH`, `ANYHOW_TRY_ASSIGN` macros for error propagation
- Short aliases via `#define ANYHOW_SHORT_MACROS`
- CMake `anyhow::anyhow` target with `find_package` and `FetchContent` support
- CMake `anyhow::anyhow` target via `FetchContent` or `find_package`
85 changes: 69 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,20 @@ 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 |
| `expected.hpp` | `Expected<T>`, `Expected<void>` |
| `failure.hpp` | `Failure`, `Unexpected`, `fail()` |
| `macros.hpp` | `ANYHOW_TRY`, `ANYHOW_TRY_ASSIGN`, `ANYHOW_TRY_CATCH` |
| `scope_guard.hpp` | `ScopeGuard` |
| 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`.
> Define `ANYHOW_SHORT_MACROS` before including `macros.hpp` to enable the short aliases `TRY`, `TRY_ASSIGN`, `TRY_CATCH`, `BAIL`, `ENSURE`.

Use `Expected<T>` as the return type of any fallible function. Return `anyhow::fail(message, domain)` on failure, or return the value directly on success.
`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.

```cpp
anyhow::Expected<int> parse(std::string_view s) {
Expand All @@ -65,6 +67,23 @@ anyhow::Expected<int> parse(std::string_view s) {
}
```

### Early return

`ANYHOW_BAIL` returns a failure immediately. `ANYHOW_ENSURE` does the same when a condition is false.

```cpp
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.
Expand All @@ -81,13 +100,13 @@ anyhow::Expected<std::string> process(std::string_view s) {
```

```cpp
auto r = process("");
if (r.failed()) {
auto& f = r.failure();
std::cout << "error [" << f.error.domain << "]: " << f.error.message << '\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 < f.count; i++) {
std::cout << " at " << f.frames[i].function << " (" << f.frames[i].file << ':' << f.frames[i].line << ")\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";
}
}
```
Expand Down Expand Up @@ -118,6 +137,36 @@ 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.

```cpp
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.

```cpp
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.
Expand All @@ -130,13 +179,17 @@ auto result = parse("21")
});
```

### Scope guard

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

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

Override the frame buffer depth at compile time (default: `16`). When full, oldest frames are evicted. Must be defined before any anyhow include.
### Frame buffer depth

Override at compile time (default: `16`). When full, oldest frames are evicted. Must be defined before any anyhow include.

```cpp
#define ANYHOW_MAX_FRAMES 32
Expand Down
76 changes: 73 additions & 3 deletions docs/vs-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ A feature-by-feature comparison between [`anyhow`](https://github.com/dtolnay/an

- [Result type](#result-type)
- [Error construction](#error-construction)
- [Early return](#early-return)
- [Propagation](#propagation)
- [Context](#context)
- [Downcasting](#downcasting)
- [Inspection](#inspection)
- [Backtraces](#backtraces)

## Result type
Expand All @@ -26,8 +29,13 @@ anyhow::Expected<Config> load(std::string_view path) {
}
```

> [!NOTE]
> A `Result<T>` alias matching Rust's naming is planned but not yet shipped.
`anyhow::Result<T>` is available as an alias for `Expected<T>`:

```cpp
anyhow::Result<Config> load(std::string_view path) {
// ...
}
```

## Error construction

Expand All @@ -41,6 +49,26 @@ return Err(anyhow!("empty input"));
return anyhow::fail("empty input", "parse");
```

## Early return

Rust's `bail!` and `ensure!` macros construct an error and return early in one step. C++ mirrors them with `ANYHOW_BAIL` and `ANYHOW_ENSURE`.

```rust
bail!("negative value");
ensure!(!s.is_empty(), "empty input");
```

```cpp
ANYHOW_BAIL("negative value");
ANYHOW_ENSURE(!s.empty(), "empty input");
```

Both accept an optional domain tag as a second argument in C++ (no direct Rust equivalent since Rust uses typed errors for that):

```cpp
ANYHOW_ENSURE(!s.empty(), "empty input", "parse");
```

## Propagation

Rust's `?` operator unwraps on success or returns the error early, converting it through `From`. C++ has no equivalent operator; propagation goes through statement macros that expand to the unwrap-or-return pattern. Each macro captures the current `std::source_location` and pushes it onto the failure's frame buffer, so the trace builds as the error travels up.
Expand All @@ -57,7 +85,7 @@ ANYHOW_TRY(validate(n));
```

> [!NOTE]
> `ANYHOW_TRY_CATCH(expr, cleanup)` has no Rust analogue -- it runs a cleanup expression before returning on failure. Define `ANYHOW_SHORT_MACROS` for the unprefixed `TRY` / `TRY_ASSIGN` / `TRY_CATCH` aliases.
> `ANYHOW_TRY_CATCH(expr, cleanup)` has no Rust analogue -- it runs a cleanup expression before returning on failure. Define `ANYHOW_SHORT_MACROS` before including `macros.hpp` to enable unprefixed aliases for all macros: `TRY`, `TRY_ASSIGN`, `TRY_CATCH`, `BAIL`, `ENSURE`.

## Context

Expand All @@ -77,6 +105,48 @@ parse_file(path)

Both render context outermost-first, then the root error.

## Downcasting

Rust erases the concrete error type behind `anyhow::Error` but lets you recover it with `downcast_ref::<E>()` / `is::<E>()`.

```rust
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
// ...
}
```

C++ has no `std::error::Error` trait, so the approach differs: attach a typed payload via `fail_with()` and recover it with `Failure::downcast<T>()`, which returns a pointer or null.

```cpp
return anyhow::fail_with(IoError {errno}, "read failed", "io");

if (auto* io = failure.downcast<IoError>()) {
// ...
}
```

The `domain` string tag remains available for cheaper string-based discrimination when a typed payload is not needed.

## Inspection

Rust walks the error's source chain via `Error::chain()` and reaches the deepest cause with `root_cause()`.

```rust
for cause in err.chain() {
eprintln!("{cause}");
}
let root = err.root_cause();
```

C++ mirrors both on `Failure`. `chain()` returns context layers outermost-first followed by the root error message. `root_cause()` returns the root `ErrorInfo` directly.

```cpp
for (auto cause : failure.chain()) {
std::cerr << cause << '\n';
}
const auto& root = failure.root_cause();
```

## Backtraces

Rust captures a `std::backtrace::Backtrace` on error creation, gated behind the `RUST_BACKTRACE` environment variable and a nightly/stable feature boundary.
Expand Down
16 changes: 13 additions & 3 deletions src/anyhow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@

/// anyhow-cpp: header-only C++20 result type with propagating stacktraced errors.
///
/// Return `Expected<T>` from fallible functions; `anyhow::fail(msg, domain)` on
/// failure. Propagate with the `ANYHOW_TRY*` macros, which push the current call
/// site onto a frame trace. See the README for usage and examples.
/// Return `Expected<T>` (or `Result<T>`) from fallible functions. Construct errors
/// with `fail(msg, domain)` or `fail_with(payload, msg, domain)` for typed payloads
/// recoverable via `Failure::downcast<T>()`. Propagate with `ANYHOW_TRY*` macros,
/// which capture `std::source_location` at each call site and push it onto the frame
/// trace. Return early unconditionally with `ANYHOW_BAIL` or conditionally with
/// `ANYHOW_ENSURE`. See the README for usage and examples.

#include "expected.hpp"
#include "failure.hpp"
#include "macros.hpp"
#include "scope_guard.hpp"

namespace anyhow {

template<typename T>
using Result = Expected<T>;

} // namespace anyhow
10 changes: 7 additions & 3 deletions src/error.hpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#pragma once

#include <any>
#include <string>

namespace anyhow {

/// Root error: a human-readable message and an optional domain tag (e.g. "io", "parse").
/// Root error: a human-readable message, an optional domain tag, and an optional typed payload.
///
/// `payload` is empty by default. Set it via `fail_with()` and recover with `Failure::downcast<T>()`.
struct ErrorInfo {
std::string message;
std::string domain;
std::string message; ///< Human-readable description of what went wrong.
std::string domain; ///< Optional category tag (e.g. "io", "parse"). Empty if unset.
std::any payload; ///< Optional typed value for programmatic inspection. Empty if unset.
};

} // namespace anyhow
Loading