Error Propagation

Forwarding errors up the call chain with the ? operator.

Intermediate⏱️ 40 min📚 Prerequisites: 1

Error Propagation

Error propagation means that when a function receives an error, it forwards it to the calling function instead of handling it locally.

? Operator

The ? operator in Rust allows automatic error propagation:

RUST
fn read_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

The ? operator:

  • If Ok(value), unwraps the value
  • If Err(error), immediately returns the Err

Manual Propagation

Before the ? operator, we had to do it manually:

RUST
fn read_file() -> Result<String, io::Error> {
    let mut f = match File::open("hello.txt") {
        Ok(file) => file,
        Err(e) => return Err(e),  // Manual propagation
    };
    // ...
}

Handling Multiple Error Types

RUST
use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let num = s.parse::<i32>()?;
    Ok(num * 2)
}

Custom Error Types

RUST
#[derive(Debug)]
enum MyError {
    ParseError,
    FileError,
}

Code Examples

Using ? Operator

Propagating errors with the ? operator

RUST
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err(String::from("Division by zero"));
    }
    Ok(a / b)
}
fn calculate(a: f64, b: f64, c: f64) -> Result<f64, String> {
    let step1 = divide(a, b)?;
    let step2 = divide(step1, c)?;
    Ok(step2)
}
fn main() {
    match calculate(100.0, 2.0, 5.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Explanation:

The ? operator automatically propagates the error. If divide returns Err, the calculate function immediately returns the Err.

Multiple Error Types

Handling different error types

RUST
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse::<i32>()
}
fn double_string(s: &str) -> Result<i32, ParseIntError> {
    let num = parse_number(s)?;
    Ok(num * 2)
}
fn main() {
    match double_string("42") {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {:?}", e),
    }
}

Explanation:

The ? operator only works if the error type is compatible. ParseIntError is automatically propagated.

Manual Propagation

Handling errors with match (before ? operator)

RUST
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err(String::from("Division by zero"));
    }
    Ok(a / b)
}
fn calculate_manual(a: f64, b: f64, c: f64) -> Result<f64, String> {
    let step1 = match divide(a, b) {
        Ok(value) => value,
        Err(e) => return Err(e),
    };
    let step2 = match divide(step1, c) {
        Ok(value) => value,
        Err(e) => return Err(e),
    };
    Ok(step2)
}
fn main() {
    match calculate_manual(100.0, 2.0, 5.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Explanation:

Manual propagation with match is longer but does the same as the ? operator. The ? operator compresses this.

Exercises

Error Propagation

Use the ? operator in a function chain!

Medium

Starter Code:

RUST
fn step1(x: i32) -> Result<i32, String> {
    if x < 0 {
        Err(String::from("Negative number"))
    } else {
        Ok(x * 2)
    }
}
fn step2(x: i32) -> Result<i32, String> {
    if x > 100 {
        Err(String::from("Number too large"))
    } else {
        Ok(x + 10)
    }
}
fn main() {
    match process(5) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}