Boxing Errors

Box<dyn Error> is the standard-library way to return heterogeneous error types when callers only need to report them.

What it is

A boxed error stores any concrete type implementing The Error Trait behind a trait object. The common signature is Result<T, Box<dyn std::error::Error>>.

This is a dependency-free middle ground between precise Custom Error Types and Application Errors with anyhow.

How it works

The standard library provides conversions that let many concrete errors flow into a boxed error with ?. A type alias can keep signatures readable.

Use Box<dyn Error + Send + Sync + 'static> when the error may cross thread boundaries or be stored in broadly reusable infrastructure. For public libraries where callers need to match variants, prefer a typed error enum. The box performs type erasure: the caller can print the error and walk source(), but the concrete type is hidden behind dynamic dispatch. Downcasting is possible only when the concrete type is 'static, and relying on it as an API contract is brittle. Boxing does not add context; it only gives different error types one return type.

Example

use std::error::Error;
use std::fs;
use std::num::ParseIntError;
 
type BoxError = Box<dyn Error + Send + Sync + 'static>;
 
fn read_number(path: &str) -> Result<u32, BoxError> {
    let text = fs::read_to_string(path)?;
    let number: u32 = parse_number(text.trim())?;
    Ok(number)
}
 
fn parse_number(input: &str) -> Result<u32, ParseIntError> {
    input.parse()
}
 
fn main() -> Result<(), BoxError> {
    let _ = read_number("number.txt");
    Ok(())
}

Second example

A boxed error works well at a run boundary that glues several small helpers together.

use std::error::Error;
use std::fs;
use std::num::ParseIntError;
 
type BoxError = Box<dyn Error + Send + Sync + 'static>;
 
fn parse_limit(text: &str) -> Result<u32, ParseIntError> {
    text.trim().parse()
}
 
fn run(path: &str) -> Result<u32, BoxError> {
    let text = fs::read_to_string(path)?;
    let limit = parse_limit(&text)?;
    Ok(limit)
}
 
fn main() {
    let _ = run("limit.txt");
}

Common errors

Threaded or async code may reject a plain boxed error:

error[E0277]: `dyn std::error::Error` cannot be sent between threads safely

Fix the alias to Box<dyn Error + Send + Sync + 'static> when the error crosses thread or task boundaries. If the error is part of a public library contract, consider a typed enum instead.

Best practice

  • ✅ Use boxed errors for examples, small binaries, build scripts, prototypes, and glue code.
  • ✅ Add Send + Sync + 'static when using boxed errors in async or threaded infrastructure.
  • ✅ Move to Application Errors with anyhow when you want ergonomic context and reporting.
  • ✅ Move to Custom Error Types when callers need stable, branchable failure modes.
  • ✅ Use a local type alias so signatures reveal intent without repeating trait-object bounds.
  • ✅ Add context before boxing if the concrete source message is too low-level.

Pitfalls

  • ⚠️ Public library APIs using boxed errors can force callers into brittle downcasting.
  • ⚠️ Box<dyn Error> does not automatically add operation context; see Adding Error Context.
  • ⚠️ Omitting Send + Sync can later block moving errors across threads.
  • ⚠️ Boxing a Stringly-Typed Errors value still leaves it unstructured.
  • ⚠️ Treating downcast success as guaranteed can break when an implementation changes its concrete source error.

See also

The Error Trait · Returning Result from main · Application Errors with anyhow · Custom Error Types · Adding Error Context · Stringly-Typed Errors · Propagating Errors · Error Handling

Sources