#error-handling #error

no-std erratic

Handling errors in an efficient way

54 releases (16 breaking)

Uses new Rust 2024

new 0.17.1 Jun 11, 2026
0.16.0 Jun 10, 2026

#430 in Rust patterns

MIT license

150KB
3.5K SLoC

Erratic /ɪˈrætɪk/

license crates.io docs.rs

This library provides Error<S = Stateless>, an error type with optional dynamic dispatch, enabling applications to handle errors uniformly across different scenarios.

Quick Start

In most cases, Error can serve as a drop-in replacement for Box<dyn Error>. Compared to the latter, it occupies only 1 usize, making the happy path faster.

fn say_hi(filename: &str) -> erratic::Result<()> {
    File::open(filename)?.write_all(b"Hello, World!")?;
    Ok(())
}

Attaching Context

When constructing an error, you can optionally attach a context. If the context is a literal string and it's the only component of the error, no heap allocation occurs.

use erratic::*;

fn read_weak(r: &mut Weak<Reader>, buf: &mut [u8]) -> Result<u64> {
    if buf.is_empty() {
        return mkres!("buf must not be empty"); // No alloc so long as no format args.
    }
    let r = r.upgrade()
        .with_context("stream expired")?; // Accepts any value implementing `Display`.
    let n = r.read(buf)
        .with_context(mkctx!("cannot read {}", r.id()))?; // `mkctx!` evaluates lazily.
    Ok(n)
}

Binding State

When propagating an error that requires special handling, you can optionally attach a state to it. If the state is small enough and it's the only component of the error, the state is inlined without any heap allocation.

use erratic::*;

#[derive(Debug)]
enum State { RetryLater }

fn try_write(w: &mut Writer, data: &[u8; 64]) -> Result<(), Error<State>> {
    w.reserve_chunk(64)
        .ok()
        .with_state(State::RetryLater)?; // No alloc.
    w.write(data)
        .with_context(mkctx!("failed to write to {}", w.id()))?;
    Ok(())
}

When no runtime state is actually stored, errors can be cheaply converted between different state types. This allows infrastructure errors to cross any number of layers with no extra allocation, domain errors avoid the heap entirely, and both share the same Error<S> type. All compose orthogonally.

fn write(w: &mut Writer, data: &[u8; 64]) -> Result<()> {
    while let Err((state, _)) = try_write(w, data).extract_state()? {
        match state {
            State::RetryLater => {
                thread::yield_now();
            }
        }
    }
    Ok(())
}

The ? operator covers the most common cases, notably including conversion from Error to Error<S>:

  • impl Error -> Error
  • impl Error -> Error<S>
  • Builder<..> -> Error
  • Builder<..> -> Error<S>
  • Error -> Error<S>

Stateful errors are meant to be handled explicitly. Several utility methods are provided:

  • erase_error()?: Propagate the error.
  • extract_state()?: Take the state out, or propagate the error.
  • map_state()?: Transform the state with a closure.
  • lift_state()?: Transform via From<S>.

Backtrace

When the backtrace feature is enabled and backtrace capture is configured via environment variables, Error<S> automatically captures a backtrace if there isn't one already in the source chain. The backtrace will be appended after the error chain during debug formatting, unless the minus flag, e.g. {:-?}, is specified to suppress it.

Representation

If the error contains only a source, the error message is inherited from the source. Otherwise, the error message is constructed from other attached components.

<error> ::= <source>
          | <state>": "<context>
          | <context>
          | <state>

By default, only the top-level error is shown during formatting. To display the full error chain, format with alternate or debug specifiers.

  • {}: Displays only the top-level error.
  • {:#}: Displays the full error chain.
  • {:?}: Displays the full error chain with backtrace (if captured).
  • {:#?}: Displays all information in a struct-like format.

The error chain is defined as follows:

<chain> ::= <error>
          | <error>"\n  -> "<chain>

Layout

Type-wise, Error<S> is an internally tagged union, and it requires pointers to be aligned to 4 bytes, freeing up the lower 2 bits to encode its discriminant. Pointer tagging in this crate fully follows strict provenance, and is verified by Miri.

(32-bit platform, little-endian)
(Context Only)
[......00|........|........|........]
                                    \
                                     `rodata-> [Context]
(Allocation Required)
[......01|........|........|........]
                                    \
                                     `heap-> [VTable|State|Error|Context]
(Small State Only)
[00000010|     ~    State     ~     ]

Contributing

Contributions are warmly welcomed! Whether you have a bug report, feature request, or an improvement in mind, feel free to open an issue or submit a pull request. All ideas—big or small—help make this library better for everyone.

No runtime deps

Features