Error Handling with Box<dyn Error>

Reviewed & published by Brayan K

Box<dyn Error> is a boxed trait object that can hold any error type, letting a single Result propagate many different kinds of failure through the ? operator with almost no boilerplate.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll use Result<T, Box<dyn Error>> with ?, implement Error + Display + Debug for a custom error, return a Result from main, and weigh it against enum errors.

What You'll Learn in This Lesson

1️⃣ One Result, Many Error Types

A function returning Result<T, Box<dyn Error>> can fail in many ways. The ? operator unwraps an Ok or returns early on an Err, automatically converting the concrete error (here a ParseIntError) into the boxed trait object via the From trait. You print the error with {} because every error implements Display.

use std::error::Error;
use std::num::ParseIntError;

// Box<dyn Error> can hold ANY error type that implements std::error::Error.
// The ? operator auto-converts a concrete error into the boxed trait object.
fn double_number(text: &str) -> Result<i32, Box<dyn Error>> {
    let n: i32 = text.parse()?;     // ParseIntError -> Box<dyn Error> automatically
    Ok(n * 2)
}

fn main() {
    match double_number("21") {
        Ok(v) => println!("ok: {}", v),       // -> ok: 42
        Err(e) => println!("error: {}", e),
    }
    match double_number("not a number") {
        Ok(v) => println!("ok: {}", v),
        Err(e) => println!("error: {}", e),   // prints the parse error message
    }

    // We can keep the concrete type around too if we want.
    let _typed: Result<i32, ParseIntError> = "7".parse();
}

The beauty is that you never wrote any conversion code — the single ? after text.parse() did the boxing. Add another fallible call with a different error type and the same ? works there too.

2️⃣ Defining Your Own Error

To create a custom error, define a type and give it three impls: Debug (usually derived), Display for the message, and std::error::Error (often an empty block). Once those are in place, your error slots into Box<dyn Error> anywhere, and ? will box it automatically.

use std::error::Error;
use std::fmt;

// A custom error: just a struct (or enum) plus Display + Debug + Error.
#[derive(Debug)]               // Debug is required by the Error trait
struct TooYoung {
    age: u32,
}

// Display defines the human-readable message.
impl fmt::Display for TooYoung {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "age {} is below the minimum of 18", self.age)
    }
}

// Implementing Error (with Debug + Display in place) is then trivial.
impl Error for TooYoung {}

fn check_age(age: u32) -> Result<(), Box<dyn Error>> {
    if age < 18 {
        // Our custom error converts into Box<dyn Error> via ? or .into().
        return Err(Box::new(TooYoung { age }));
    }
    Ok(())
}

fn main() {
    for age in [25, 16] {
        match check_age(age) {
            Ok(()) => println!("age {}: allowed", age),
            Err(e) => println!("age {}: {}", age, e),
        }
    }
}

Debug is required by the Error trait (it's what gets printed if the error reaches main), and Display is the friendly message you control.

3️⃣ Returning Result from main

main itself can return Result<(), Box<dyn Error>>. That lets you use ? right inside main: on success you return Ok(()), and on an error the program exits with a non-zero status and prints the error's Debug form.

use std::error::Error;

// main can return Result! On Err, the program exits non-zero
// and prints the Debug form of the error.
fn parse_and_sum(parts: &[&str]) -> Result<i32, Box<dyn Error>> {
    let mut total = 0;
    for p in parts {
        total += p.parse::<i32>()?;   // ? bubbles up any parse error
    }
    Ok(total)
}

fn main() -> Result<(), Box<dyn Error>> {
    let sum = parse_and_sum(&["10", "20", "12"])?;
    println!("sum = {}", sum);        // -> sum = 42
    println!("all parsed successfully");
    Ok(())                            // success path returns Ok(())
}

This is the idiomatic shape of small Rust programs: helper functions return Result<_, Box<dyn Error>>, main does too, and ? threads errors all the way to the top.

Your turn. Fill in the blank marked ___, then run it.

// 🎯 YOUR TURN — return the first char, or a boxed error if empty.
use std::error::Error;

fn first_char(s: &str) -> Result<char, Box<dyn Error>> {
    // chars().next() gives Option<char>; turn None into an error,
    // then use ? to unwrap or bubble it up.
    let c = s.chars().next().ok_or("string was empty")___;  // 👉 the bubble-up operator
    Ok(c)
}

fn main() {
    match first_char("rust") {
        Ok(c) => println!("first char: {}", c),
        Err(e) => println!("error: {}", e),
    }
    match first_char("") {
        Ok(c) => println!("first char: {}", c),
        Err(e) => println!("error: {}", e),
    }
}

// ✅ Expected output:
//    first char: r
//    error: string was empty

Mini-Challenge: safe_divide

Write a function that can fail two different ways — a parse error and a divide-by-zero — both flowing through one Box<dyn Error>. Run it with cargo run and check the output.

// 🎯 MINI-CHALLENGE: a safe_divide that handles two error sources.
//
// Write fn safe_divide(a: &str, b: &str) -> Result<i32, Box<dyn Error>>:
//   1. Parse a and b into i32 using ? (a parse failure becomes the error).
//   2. If the divisor is 0, return Err("cannot divide by zero".into()).
//   3. Otherwise return Ok(x / y).
//
// In main, run it over ("20","5"), ("9","0"), and ("x","2"),
// printing each result or its error message.
//
// ✅ Expected output:
//    20 / 5 = 4
//    9 / 0 -> error: cannot divide by zero
//    x / 2 -> error: invalid digit found in string

use std::error::Error;

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Box<dyn Error>

ItemPurpose
Result<T, Box<dyn Error>>Hold any error type
expr?Unwrap or return + convert error
"msg".into()Quick string error
impl Error for E {}After Debug + Display
fn main() -> Result<...>Use ? in main

🎉 Lesson Complete!

Practice quiz

What is a Box<dyn Error>?

  • A fixed error enum
  • A panic handler
  • A heap-allocated trait object that can hold any type implementing the Error trait
  • A stack-only value

Answer: A heap-allocated trait object that can hold any type implementing the Error trait. It is a boxed trait object: any std::error::Error type behind a pointer, with the concrete type erased.

What does the dyn keyword signify in Box<dyn Error>?

  • The concrete type is decided at runtime (dynamic dispatch)
  • The error is dynamic-sized only
  • The error is dangerous
  • The value is never dropped

Answer: The concrete type is decided at runtime (dynamic dispatch). dyn marks a trait object whose concrete type is resolved at runtime, so one Result can carry many error types.

On an Err, before returning, what does the ? operator do to the error?

  • Ignores it
  • Panics
  • Logs it to a file
  • Converts it using the From trait

Answer: Converts it using the From trait. ? converts the error via From, which is why a concrete error like ParseIntError boxes into Box<dyn Error> automatically.

Which three things must a custom error type provide?

  • new, drop, clone
  • Debug, Display, and impl Error
  • Copy, Default, Hash
  • Iterator, Send, Sync

Answer: Debug, Display, and impl Error. A custom error needs Debug (required by Error), Display for the message, and an impl of the Error trait.

The Error trait requires which other trait to be implemented?

  • Debug
  • Clone
  • Copy
  • Default

Answer: Debug. Debug is required by the Error trait; it is what prints if the error reaches main.

What does Display provide for a custom error?

  • The Debug braces output
  • A clone method
  • The human-readable message
  • The error code

Answer: The human-readable message. Display defines the friendly, human-readable message you control.

Can a plain &str be turned into a Box<dyn Error>?

  • No, never
  • Yes, e.g. return Err("boom".into())
  • Only with a macro
  • Only for u32 errors

Answer: Yes, e.g. return Err("boom".into()). A &str or String already converts into Box<dyn Error>, so Err("boom".into()) works for quick errors.

What signature lets you use ? directly inside main?

  • fn main()
  • fn main() -> i32
  • fn main() -> Option<()>
  • fn main() -> Result<(), Box<dyn Error>>

Answer: fn main() -> Result<(), Box<dyn Error>>. When main returns Result<(), Box<dyn Error>> you can use ? at the top level; success returns Ok(()).

When main returns Result and errors out, what is printed and what is the exit status?

  • The Display form, exit 0
  • The Debug form of the error, non-zero exit
  • Nothing, exit 0
  • A stack trace, exit 1 always with no error

Answer: The Debug form of the error, non-zero exit. On Err, the program prints the error's Debug form and exits with a non-zero status.

When is Box<dyn Error> preferred over a concrete enum error?

  • In libraries needing exact types
  • When you must match on each variant
  • In application code and binaries that mostly propagate errors to main
  • Never

Answer: In application code and binaries that mostly propagate errors to main. Use Box<dyn Error> in apps for short signatures; use enum errors in libraries where callers inspect specific failures.

Continue this course

Frequently asked questions

What does Box<dyn Error> actually mean?

Box<dyn Error> is a boxed trait object: a heap-allocated value that can hold any type implementing the std::error::Error trait, hidden behind a pointer. The dyn keyword means the concrete type is decided at runtime, so a single Result<T, Box<dyn Error>> can carry a parse error from one line and an I/O error from another. It is the simplest way to write functions that can fail for several different reasons without defining a giant enum up front. The trade-off is that you lose the exact compile-time type of the error, which is fine for applications but less ideal for libraries.

How does the ? operator convert between error types?

When you write expression?, Rust unwraps an Ok value or, on an Err, returns early from the function. Crucially, before returning it converts the error using the From trait. Because every standard error type can be converted into Box<dyn Error>, a function returning Result<T, Box<dyn Error>> can use ? on a ParseIntError, an io::Error, and many others, all in the same function — each is automatically boxed. This is what makes Box<dyn Error> so ergonomic: you sprinkle ? everywhere and the conversions just happen.

How do I create my own error type?

Define a struct or enum to represent the error, then implement three things. First, derive or implement Debug, which the Error trait requires. Second, implement std::fmt::Display to provide the human-readable message. Third, implement std::error::Error, which can be an empty impl block once Debug and Display are in place. With those in hand, your custom error works anywhere a Box<dyn Error> is expected, and the ? operator will box it automatically. For richer errors you can also implement the source method to report an underlying cause.

When should I use a Box<dyn Error> instead of an enum error?

Use Box<dyn Error> in application code and binaries, where you mostly want to propagate errors up to main and print them — it keeps signatures short and lets ? mix many error types freely. Use a concrete enum error (one variant per failure mode) in libraries and anywhere callers need to inspect and handle specific failures differently, because an enum preserves the exact type and enables exhaustive matching. Many real projects use crates like thiserror to write enum errors with less boilerplate, and anyhow to make Box<dyn Error>-style handling even smoother in applications.