Web Analytics

Error Propagation

Intermediate ~30 min read

The ? operator is Rust's way of propagating errors automatically. Instead of writing verbose match expressions to handle every Result, you can use ? to automatically return errors to the caller. This makes error handling much more concise and readable.

What is the ? Operator?

The ? operator is a shorthand for propagating errors. When placed after a Result value, it:

  • If the value is Ok, it unwraps the value and continues
  • If the value is Err, it returns the error from the function

Before: Verbose Error Handling

Without the ? operator, you need to write verbose match expressions:

use std::fs::File;
use std::io::Read;

fn read_username_from_file() -> Result<String, std::io::Error> {
    let mut f = match File::open("hello.txt") {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

After: Using the ? Operator

With the ? operator, the same code becomes much more concise:

use std::fs::File;
use std::io::Read;

fn read_username_from_file() -> Result<String, std::io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
Much Cleaner: The ? operator reduces boilerplate code significantly, making error handling more readable and maintainable.
Output
Click Run to execute your code

How ? Works

The ? operator is syntactic sugar for:

match result {
    Ok(value) => value,
    Err(error) => return Err(error.into()),
}

Notice that it uses error.into(), which means it can convert between compatible error types automatically!

Automatic Conversion: The ? operator uses Into trait to convert error types, allowing you to use different error types in the same function chain.

Using ? with Different Error Types

You can use ? with different error types by using Box<dyn std::error::Error>:

use std::fs::File;
use std::io::Read;

fn parse_and_read() -> Result<i32, Box<dyn std::error::Error>> {
    let mut s = String::new();
    File::open("number.txt")?
        .read_to_string(&mut s)?;
    let num: i32 = s.trim().parse()?;
    Ok(num)
}
Box<dyn Error>: This type can hold any error type that implements the Error trait, allowing you to mix different error types in one function.

Chaining Operations with ?

You can chain multiple operations that return Result:

fn read_file_contents(filename: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

Or even more concisely:

fn read_and_parse(filename: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string(filename)?;
    let number: i32 = contents.trim().parse()?;
    Ok(number)
}
Output
Click Run to execute your code

Using ? in main()

You can use ? in main() by changing its return type:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let username = read_username_from_file()?;
    println!("Username: {}", username);
    Ok(())
}
main() Return Type: When main() returns Result, Rust will print the error and exit with a non-zero status code if an error occurs.

When Can You Use ?

The ? operator can only be used in functions that return Result (or Option). It cannot be used in functions that return other types.

// โœ… OK - Function returns Result
fn read_file() -> Result<String, std::io::Error> {
    let contents = std::fs::read_to_string("file.txt")?;
    Ok(contents)
}

// โŒ Error - Function doesn't return Result
fn read_file() -> String {
    let contents = std::fs::read_to_string("file.txt")?;  // Error!
    contents
}

Converting Error Types

The ? operator automatically converts error types using the Into trait:

use std::num::ParseIntError;

fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn read_and_parse() -> Result<i32, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string("number.txt")?;  // io::Error
    let number = contents.trim().parse()?;  // ParseIntError
    Ok(number)
}
Error Conversion: Both io::Error and ParseIntError can be converted to Box<dyn Error> automatically, allowing you to use ? with different error types.

Best Practices

  • Use ? for error propagation: Prefer ? over verbose match expressions
  • Use Box<dyn Error> for mixed errors: When combining different error types
  • Keep functions focused: Let errors propagate to the caller
  • Add context when needed: Use map_err() before ? to add context
  • Use ? in main(): Change main() return type to Result for cleaner error handling
  • Document error conditions: Make it clear what errors your function can return

Common Mistakes

1. Using ? in functions that don't return Result

// Wrong - Function doesn't return Result
fn read_file() -> String {
    let contents = std::fs::read_to_string("file.txt")?;  // Error!
    contents
}

// Correct - Function returns Result
fn read_file() -> Result<String, std::io::Error> {
    let contents = std::fs::read_to_string("file.txt")?;
    Ok(contents)
}

2. Not handling incompatible error types

// Wrong - Different error types
fn process() -> Result<i32, std::io::Error> {
    let num: i32 = "5".parse()?;  // Error! ParseIntError != io::Error
    Ok(num)
}

// Correct - Use Box<dyn Error>
fn process() -> Result<i32, Box<dyn std::error::Error>> {
    let num: i32 = "5".parse()?;
    Ok(num)
}

3. Forgetting to return Ok() at the end

// Wrong - Missing Ok()
fn read_file() -> Result<String, std::io::Error> {
    let contents = std::fs::read_to_string("file.txt")?;
    contents  // Error! Should be Ok(contents)
}

// Correct - Return Ok()
fn read_file() -> Result<String, std::io::Error> {
    let contents = std::fs::read_to_string("file.txt")?;
    Ok(contents)
}

Exercise: Error Propagation Practice

Task: Implement functions using the ? operator for error propagation.

Requirements:

  • Read and parse numbers from files
  • Chain multiple operations with ?
  • Handle different error types
  • Use Box<dyn Error> for mixed error types
Output
Click Run to execute your code
Show Solution
use std::fs::File;
use std::io::Read;

fn read_number_from_file(filename: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let mut file = File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let number: i32 = contents.trim().parse()?;
    Ok(number)
}

fn read_divide(filename: &str) -> Result<f64, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string(filename)?;
    let divisor: i32 = contents.trim().parse()?;
    if divisor == 0 {
        return Err("Division by zero".into());
    }
    Ok(100.0 / divisor as f64)
}

fn validate_and_divide(n: i32) -> Result<f64, String> {
    if n <= 0 {
        return Err(String::from("Number must be positive"));
    }
    if n == 0 {
        return Err(String::from("Division by zero"));
    }
    Ok(100.0 / n as f64)
}

Summary

  • ? operator propagates errors automatically
  • Can only be used in functions that return Result or Option
  • Unwraps Ok values, returns Err values
  • Automatically converts error types using Into trait
  • Use Box<dyn Error> for mixed error types
  • Much more concise than verbose match expressions
  • Can be used in main() by changing return type
  • Chains multiple operations cleanly
  • Remember to return Ok() at the end

What's Next?

Now that you understand error propagation, you'll learn about custom error types. Creating your own error types allows you to provide better error messages, add context, and make your error handling more structured and maintainable.