Error Handling with ? & Custom Errors

Rust · Lesson 14 of 31 7 min read Updated

In the last lesson you handled every Result with match. That works, but a function that calls five operations which can each fail would need five match blocks, and the real logic would drown in error handling. Rust's answer is the ? operator, which passes an error up to the caller in a single character.

In this lesson you will use ? with Result and Option, let main return errors, design your own error type that callers can match on, and learn when a panic is the right choice.

Panic or Result?

Rust separates two kinds of failure:

  • Unrecoverable problems are bugs: an index that should never be out of range, a state your program logic says is impossible. Call panic!("message") (or use unwrap/expect); the program stops and prints where it happened.
  • Recoverable problems are things that can go wrong in normal use: a user types letters into a number field, a file is missing, the network is down. Return a Result so the caller can decide what to do.

As a rule, library code should almost never panic on bad input. Return a Result and let the application choose: retry, ask the user again or show a message.

The ? operator

Put ? after an expression that returns a Result. If it is Ok(value), ? unwraps it and the code continues with value. If it is Err(e), the function returns immediately with that error.

Rust
use std::num::ParseIntError;

fn total_marks(theory: &str, practical: &str) -> Result<u32, ParseIntError> {
    let t: u32 = theory.trim().parse()?;
    let p: u32 = practical.trim().parse()?;
    Ok(t + p)
}

fn main() {
    println!("{:?}", total_marks("45", " 38 "));
    println!("{:?}", total_marks("45", "3x"));
}
Output
Ok(83)
Err(ParseIntError { kind: InvalidDigit })

Without ?, each parse would need its own match with an Err(e) => return Err(e) arm; ? writes exactly that for you. Two conditions apply: ? can only be used inside a function that itself returns Result (or Option), and the error must be convertible into the function's error type (more on that below).

? works on Option too, in functions returning Option: on None it returns None at once.

Rust
fn initials(full_name: &str) -> Option<String> {
    let mut words = full_name.split_whitespace();
    let first = words.next()?.chars().next()?;
    let last = words.next()?.chars().next()?;
    Some(format!("{first}.{last}."))
}

fn main() {
    println!("{:?}", initials("Priya Sharma"));
    println!("{:?}", initials("Priya"));
}
Output
Some("P.S.")
None

For "Priya" the second words.next() is None, so the function stops there and returns None.

Building your own error type

A String error is easy to create but hard to work with: callers cannot tell a "bad number" from an "out of stock" problem without comparing text. A custom error enum gives each failure its own variant. To be a good citizen, an error type should implement three traits (you will study traits properly in Traits):

  • Debug (derived), so it can be printed with {:?};
  • Display, a user-friendly message for {};
  • std::error::Error, the standard marker for error types.

Implementing From<OtherError> lets ? convert other errors into yours automatically.

Rust
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum OrderError {
    BadQuantity(ParseIntError),
    OutOfStock { item: String, available: u32 },
    EmptyOrder,
}

impl fmt::Display for OrderError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OrderError::BadQuantity(e) => write!(f, "quantity is not a number ({e})"),
            OrderError::OutOfStock { item, available } => {
                write!(f, "only {available} {item} left in stock")
            }
            OrderError::EmptyOrder => write!(f, "order has no items"),
        }
    }
}

impl std::error::Error for OrderError {}

impl From<ParseIntError> for OrderError {
    fn from(e: ParseIntError) -> Self {
        OrderError::BadQuantity(e)
    }
}

fn place_order(item: &str, qty_text: &str, stock: u32) -> Result<u32, OrderError> {
    let qty: u32 = qty_text.trim().parse()?; // ParseIntError becomes OrderError
    if qty == 0 {
        return Err(OrderError::EmptyOrder);
    }
    if qty > stock {
        return Err(OrderError::OutOfStock { item: item.to_string(), available: stock });
    }
    Ok(stock - qty)
}

fn main() {
    let orders = [("samosa", "12"), ("jalebi", "40"), ("lassi", "two"), ("chai", "0")];
    for (item, qty) in orders {
        match place_order(item, qty, 30) {
            Ok(left) => println!("{item}: order placed, {left} left"),
            Err(e) => println!("{item}: failed, {e}"),
        }
    }
}
Output
samosa: order placed, 18 left
jalebi: failed, only 30 jalebi left in stock
lassi: failed, quantity is not a number (invalid digit found in string)
chai: failed, order has no items

write! works like format! but writes into the formatter f. The <'_> after Formatter is a lifetime marker, which you can ignore until the Lifetimes lesson. The empty impl std::error::Error for OrderError {} is enough, because the trait's methods all have default versions. A caller can now match on OrderError::OutOfStock { available, .. } and, for example, offer the available quantity instead.

Returning errors from main

main may return Result<(), E>. If it returns Err, Rust prints Error: followed by the error's Debug form and exits with a non-zero exit code, which scripts and CI servers understand as failure. The type Box<dyn Error> means "any error type", and ? converts every standard error into it:

Rust
use std::error::Error;

// OrderError and place_order as above

fn main() -> Result<(), Box<dyn Error>> {
    let left = place_order("samosa", "12", 30)?;
    println!("Stock left: {left}");
    let left = place_order("lassi", "two", 30)?;
    println!("Stock left: {left}");
    Ok(())
}
Output
Stock left: 18
Error: BadQuantity(ParseIntError { kind: InvalidDigit })

This is convenient in small programs. Applications usually print a friendlier message themselves, or use the anyhow crate, which you will meet in Popular Crates: serde, clap & anyhow.

Less typing with thiserror

Writing Display and From by hand gets repetitive. The widely used thiserror crate (version 2, add it with cargo add thiserror) generates them from attributes. This is the same OrderError as above:

Rust
use thiserror::Error;

#[derive(Debug, Error)]
enum OrderError {
    #[error("quantity is not a number ({0})")]
    BadQuantity(#[from] std::num::ParseIntError),
    #[error("only {available} {item} left in stock")]
    OutOfStock { item: String, available: u32 },
    #[error("order has no items")]
    EmptyOrder,
}

#[error("...")] becomes the Display text, where {0} is the first unnamed field and {item} a named one, and #[from] writes the From conversion. A common split: libraries define precise errors with thiserror, and applications collect them with anyhow.

Common mistakes

  • Using ? in a function that returns nothing. ? needs a Result or Option return type. Change the signature, for example to -> Result<(), Box<dyn Error>>.
  • Mismatched error types. If ? cannot convert an error, the compiler says the trait From<...> is not implemented. Add a From impl, use map_err, or use Box<dyn Error>.
  • Using String for every error. Callers cannot react to specific failures. Use an enum for errors that callers may want to handle differently.
  • Panicking in library code on bad input. Return a Result and let the caller decide.

Key points

  • Panic for bugs and impossible states; return Result for failures that can happen in normal use.
  • expr? unwraps Ok/Some or returns the Err/None from the current function immediately.
  • ? converts errors using From, so implementing From<OtherError> for your error type makes conversion automatic.
  • A good custom error is an enum that derives Debug and implements Display and std::error::Error; thiserror generates this code.
  • main can return Result<(), Box<dyn Error>>; an Err prints Error: ... and exits with a failure code.

Exercise

Write fn parse_time(text: &str) -> Result<(u32, u32), TimeError> for a train timetable that turns "14:35" into (14, 35). Create an enum TimeError with variants MissingColon, BadNumber(ParseIntError) and OutOfRange, implement Display, std::error::Error and From<ParseIntError>, and use ? for the number parsing. Test it with "14:35", "1435", "ab:10" and "25:00".

Show solution
Rust
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum TimeError {
    MissingColon,
    BadNumber(ParseIntError),
    OutOfRange,
}

impl fmt::Display for TimeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TimeError::MissingColon => write!(f, "expected HH:MM"),
            TimeError::BadNumber(e) => write!(f, "bad number: {e}"),
            TimeError::OutOfRange => write!(f, "hour or minute out of range"),
        }
    }
}

impl std::error::Error for TimeError {}

impl From<ParseIntError> for TimeError {
    fn from(e: ParseIntError) -> Self {
        TimeError::BadNumber(e)
    }
}

fn parse_time(text: &str) -> Result<(u32, u32), TimeError> {
    let Some((h, m)) = text.split_once(':') else {
        return Err(TimeError::MissingColon);
    };
    let hour: u32 = h.parse()?;
    let minute: u32 = m.parse()?;
    if hour > 23 || minute > 59 {
        return Err(TimeError::OutOfRange);
    }
    Ok((hour, minute))
}

fn main() {
    for t in ["14:35", "1435", "ab:10", "25:00"] {
        match parse_time(t) {
            Ok((h, m)) => println!("{t} -> {h} hours {m} minutes"),
            Err(e) => println!("{t} -> error: {e}"),
        }
    }
}
Output
14:35 -> 14 hours 35 minutes
1435 -> error: expected HH:MM
ab:10 -> error: bad number: invalid digit found in string
25:00 -> error: hour or minute out of range

split_once(':') returns Some((before, after)) or None, which let ... else turns into the MissingColon error. The ? after each parse converts a ParseIntError into TimeError::BadNumber through the From impl.

Quick Quiz

1. What does let n: u32 = text.parse()?; do when parsing fails?

2. Where can you use the ? operator?

3. Why implement From<ParseIntError> for your own error type?

4. What does a program print when main returns Err(e)?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call