Skip to main content

Crate errortools

Crate errortools 

Source
Expand description

§errortools

crates doc

Tired of writing this in every project?

fn main() {
    if let Err(e) = run() {
        eprintln!("error: {e}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), MyError> { todo!() }

Because returning Result from main uses Debug, which gives you this:

Error: Outer(Inner(Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })))

We have a solution: MainResult.

§Example

use errortools::MainResult;
use std::{fs, io};

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("failed to load config")]
    Config(#[source] io::Error),
}

fn main() -> MainResult<Error> {
    fs::read_to_string("missing.toml").map_err(Error::Config)?;
    Ok(())
}

Output:

Error: failed to load config: No such file or directory (os error 2)

The error and its full source chain print joined with ": ". No run() wrapper, no manual loop.

§Tree format

Prefer a multi-line view? Swap the format strategy:

use errortools::{MainResult, Tree};
use std::{fs, io};

#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("failed to load config")]
    Config(#[source] io::Error),
}

fn main() -> MainResult<AppError, Tree> {
    let _ = fs::read_to_string("missing.toml").map_err(AppError::Config)?;
    Ok(())
}
Error: failed to load config
└── No such file or directory (os error 2)

§Adding context

Ever needed to wrap io::Error just to attach a path? Or keep a retry attempt around? That’s what WithContext<C, E> is for. No more ad-hoc single-variant wrappers that mess up error chains. WithContext holds a context value next to an error. The pair displays through whatever strategy you pick: Colon by default, PathColon if the context is a path. FormatError skips the wrapped error itself when it walks the chain, so it never shows up twice.

PathColon calls Path::display for you, so &Path and PathBuf go straight in. The WithPath alias names the type:

use errortools::{MainResult, WithContext, with_context::WithPath};
use std::{fs::File, io, path::Path};

#[derive(Debug, thiserror::Error)]
#[error("failed to create file")]
struct Error(#[from] WithPath<&'static Path, io::Error>);

fn main() -> MainResult<Error> {
    let path = Path::new("no/such/dir/foo.txt");
    File::create(path).map_err(|e| Error::from(WithContext::new(path, e)))?;
    Ok(())
}
Error: failed to create file: no/such/dir/foo.txt: No such file or directory (os error 2)

Retry attempt numbers fit too. The default Colon strategy takes any Display pair, and usize is Display:

fn create_with_retry(
    path: &Path,
    attempts: NonZeroUsize,
) -> Result<File, WithContext<usize, io::Error>> {
    let last = attempts.get();
    for _ in 1..last {
        if let Ok(f) = File::create(path) { return Ok(f); }
    }
    File::create(path).map_err(|e| WithContext::new(last, e))
}

You can nest the two: wrap a WithContext<usize, io::Error> inside a WithPath<&Path, WithContext<usize, io::Error>> and the chain prints <path>: <attempt>: <io error>. The with_context example shows that through MainResult end-to-end.

Need a different look? WithContext formats through any F: Format<WithContext<C, E, F>>, so there are two ways to customize it:

  1. Compose with the built-in field extractors and separators: type SpacePair = WithSpace<ContextField, ErrorField>; swaps ": " for a single space. Same recipe for any delimiter you can write as a Format tag.
  2. Write a one-shot impl when the layout is unusual: impl<C: Display, E: Display, F> Format<WithContext<C, E, F>> for MyFmt { ... }. You declare your own bounds — Colon asks for Display, PathColon asks for AsRef<Path>, you ask for whatever you need.

§But why?

Countless hours of debugging with unordered error and debug logs that may mention the needed context (such as a path), simply because it felt like too much effort to write a wrapper type just to add it.

§My strong point

It must be possible to pinpoint the exact location of an error from a single, perhaps rather long but informative, error message.

§Logging in place

Sometimes you cannot return and need to log the full source chain right where the error happens. The FormatError extension trait works on any error:

use errortools::FormatError;

if let Err(e) = do_thing() {
    tracing::error!("do_thing failed: {}", e.one_line());
    // do_thing failed: outer: middle: inner
}

For ad-hoc strategies, pick the format inline with formatted::<F>():

use errortools::{FormatError, Tree};

if let Err(e) = do_thing() {
    eprintln!("{}", e.formatted::<Tree>());
    // outer
    // └── middle
    //     └── inner
}

§Custom formats

Implement the Format<E> trait on a unit type. E is generic so your strategy can require extra bounds on the error type (e.g. Suggest for the suggestion strategy):

use core::{error::Error, fmt};
use errortools::{Format, FormatError, chain};
use itertools::Itertools;

struct Arrow;
impl<E: Error + ?Sized> Format<E> for Arrow {
    fn fmt(error: &E, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", chain(&error).format(" -> "))
    }
}

println!("{}", my_error.formatted::<Arrow>()); // outer -> middle -> inner

§Combining strategies

Add<L, R> glues two Format strategies together. Both run against the same value, left then right. There’s no built-in separator, drop a separator strategy (NewLine, Space, Colon, ColonSpace, Empty) in between, or reach for the three-arg WithSep<L, Sep, R> alias when you’d otherwise nest:

use errortools::{Formatted, OneLine, Suggestion, separator::{NewLine, WithSep}};

// Same as Add<Add<OneLine, NewLine>, Suggestion>. Renders:
// "<one-line chain>\n<top-level suggestion>"
type Brief = WithSep<OneLine, NewLine, Suggestion>;

eprintln!("{}", Formatted::<_, Brief>::new(err));

For the common separators there are zero-think aliases — WithSpace<L, R>, WithNewLine<L, R>, WithColonSpace<L, R> — all in errortools::separator:

use errortools::{Formatted, OneLine, Suggestion, separator::WithNewLine};

type Brief = WithNewLine<OneLine, Suggestion>;
eprintln!("{}", Formatted::<_, Brief>::new(err));

Bounds compose: Add<OneLine, Suggestion> only implements Format<E> when E: Error + Suggest, because Suggestion’s impl carries that bound.

The same combinator powers the WithContext default — Colon is just a type alias for WithColonSpace<ContextField, ErrorField>, where ContextField/ErrorField are extractor strategies that read the pair’s fields. To get a different delimiter, swap one piece:

use errortools::{WithContext, separator::WithSpace, with_context::{ContextField, ErrorField}};

type SpacePair = WithSpace<ContextField, ErrorField>;
let w = WithContext::<_, _, SpacePair>::new("step", "boom");
assert_eq!(w.to_string(), "step boom");

§Suggestions

For “Did you mean…” hints, implement Suggest on your error type and call error.suggestion():

use core::fmt;
use errortools::{FormatError, Suggest};

#[derive(Debug, thiserror::Error)]
enum Error {
    #[error("Config file missing")]
    NoConfig,
    #[error("Network down")]
    Network,
}

impl Suggest for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NoConfig => f.write_str("Did you copy config.example.toml to config.toml?"),
            Self::Network => Ok(()),
        }
    }
}

eprintln!("{}\n{}", Error::NoConfig.one_line(), Error::NoConfig.suggestion());
// Config file missing
// Did you copy config.example.toml to config.toml?

Only the top-level error’s hint is printed, the source chain isn’t walked. This decision is intentional: The underlying hint may be irrelevant in the context of the top-level error, and printing it may just add noise.

The idea is that every error that is supposed to have a suggestion should implement Suggest and then later the top-level error’s suggestion may concatenate the inner hint if it’s relevant with nesting matching the error chain.

§How it works

MainResult<E, F> is a type alias:

use errortools::{DisplaySwapDebug, Formatted, OneLine};

pub type MainResult<E, F = OneLine, T = ()> = Result<T, DisplaySwapDebug<Formatted<E, F>>>;

DisplaySwapDebug swaps the Debug and Display impls of its inner type. When main prints the error via Debug, it ends up reaching the Display output instead, formatted by the chosen strategy. ? converts your error automatically via the blanket From impl.

§Examples

Runnable examples in examples/:

ExampleWhat it shows
one_lineMainResult with default OneLine format
treeMainResult<E, Tree> for indented multi-line output
format_errorFormatError trait for ad-hoc formatting
custom_formatA custom Format strategy
transparent#[error(transparent)] pass-through with #[from]
with_contextWithContext tags an inner error with a context value, lifted via #[from]

Run with: cargo run --example <name>.

§Features

FeatureDefaultEffect
stdyesEnables itertools/use_std. Disable for no_std.

Re-exports§

pub use path_display::DisplayPath;
pub use with_context::WithContext;

Modules§

path_display
Display-adapter wrapper for Path-like values. This is an experimental helper module. Prefer composing a path-aware Format strategy for WithContext via ContextPath — see PathColon and WithPath for the canonical use.
separator
Separator strategies for Add.
with_context
Context-tagged error pair.

Structs§

Add
Combines two Format strategies, rendering L then R against the same value.
DisplaySwapDebug
Wrapper that swaps an inner type’s fmt::Debug and fmt::Display impls.
Formatted
An error wrapper that uses a static Format strategy for fmt::Display.
OneLine
One-line format. Joins the error and its sources with ": ".
Suggestion
Format strategy that renders the top-level error’s Suggestion hint.
Tree
Tree format with a configurable marker and indent.
TreeIndent
Default tree indent: four spaces.
TreeMarker
Default tree branch marker: "└── ".

Traits§

Format
A static strategy for formatting a value to a fmt::Formatter.
FormatError
A helper trait to format errors.
Suggest
A suggestion for how to fix an error.

Functions§

chain
Iterator over an error and its source chain.

Type Aliases§

MainResult
A result type that wraps an error with Formatted and DisplaySwapDebug to output from the main function.
MainResultWithSuggestion
A result type that wraps an error with Formatted and DisplaySwapDebug to output from the main function, with an additional suggestion.
WithSuggestion
A helper type to combine an error format strategy F with a suggestion, separated by Sep. Used by MainResultWithSuggestion to render the error and suggestion together. F defaults to OneLine and Sep defaults to a newline, but you can customize both to achieve different layouts.