Expand description
§errortools
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:
- 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 aFormattag. - 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 —Colonasks forDisplay,PathColonasks forAsRef<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/:
| Example | What it shows |
|---|---|
one_line | MainResult with default OneLine format |
tree | MainResult<E, Tree> for indented multi-line output |
format_error | FormatError trait for ad-hoc formatting |
custom_format | A custom Format strategy |
transparent | #[error(transparent)] pass-through with #[from] |
with_context | WithContext tags an inner error with a context value, lifted via #[from] |
Run with: cargo run --example <name>.
§Features
| Feature | Default | Effect |
|---|---|---|
std | yes | Enables 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-awareFormatstrategy forWithContextviaContextPath— seePathColonandWithPathfor the canonical use. - separator
- Separator strategies for
Add. - with_
context - Context-tagged error pair.
Structs§
- Add
- Combines two
Formatstrategies, renderingLthenRagainst the same value. - Display
Swap Debug - Wrapper that swaps an inner type’s
fmt::Debugandfmt::Displayimpls. - Formatted
- An error wrapper that uses a static
Formatstrategy forfmt::Display. - OneLine
- One-line format. Joins the error and its sources with
": ". - Suggestion
Formatstrategy that renders the top-level error’sSuggestionhint.- Tree
- Tree format with a configurable marker and indent.
- Tree
Indent - Default tree indent: four spaces.
- Tree
Marker - Default tree branch marker:
"└── ".
Traits§
- Format
- A static strategy for formatting a value to a
fmt::Formatter. - Format
Error - 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§
- Main
Result - A result type that wraps an error with Formatted and DisplaySwapDebug to output from the
mainfunction. - Main
Result With Suggestion - A result type that wraps an error with Formatted and DisplaySwapDebug to output from the
mainfunction, with an additional suggestion. - With
Suggestion - A helper type to combine an error format strategy
Fwith a suggestion, separated bySep. Used byMainResultWithSuggestionto render the error and suggestion together.Fdefaults toOneLineandSepdefaults to a newline, but you can customize both to achieve different layouts.