Error Handling with Result: Propagating Failures Safely

Rust handles recoverable errors using the Result type, which represents either Ok(T) for success or Err(E) for failure. This pattern forces you to handle errors explicitly, avoiding silent failures or panics.

Instead of crashing your program or hiding errors, Rust makes you deal with them. This is not just for safety, but also makes your code clearer and more reliable. The Result type is how Rust returns errors from functions that might fail, such as file access, network calls, or number parsing.

In this post, I will show you how to use Result, match on it, and write functions that return Result values cleanly.

If you have not read about Enums or Option, those topics will help you understand this one faster.

What is Result?

Result is defined like this in the standard library:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

It is a generic enum that holds:

  • Ok(T) — the success value
  • Err(E) — the error value

Matching on Result

You use match to check what happened:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

let result = divide(10.0, 2.0);

match result {
    Ok(v) => println!("Result: {}", v),
    Err(e) => println!("Error: {}", e),
}

This way, your program handles both paths safely.

Using if let

If you only care about success:

if let Ok(v) = divide(10.0, 2.0) {
    println!("Answer: {}", v);
}

If you only care about failure:

if let Err(e) = divide(10.0, 0.0) {
    println!("Failed: {}", e);
}

This is good for simple error handling.

unwrap and expect

Use unwrap() if you are sure the result is Ok:

let value = divide(10.0, 2.0).unwrap();

Use expect() to add a custom error message:

let value = divide(10.0, 2.0).expect("Division failed");

These are fine in quick programs or tests but not safe for production, as they will panic on Err.

Using the ? Operator

The ? operator is a shortcut to return early on error:

fn safe_divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        return Err("Divide by zero".to_string());
    }
    Ok(a / b)
}

Now use this in another function:

fn process() -> Result<(), String> {
    let result = safe_divide(10.0, 0.0)?;
    println!("Result: {}", result);
    Ok(())
}

If safe_divide returns Err, the function exits immediately. This keeps code clean.

Combining Results with map and and_then

Transform a Result with map:

let val = divide(10.0, 2.0).map(|v| v * 2.0);

Chain operations with and_then:

let res = divide(10.0, 2.0).and_then(|v| divide(v, 2.0));

These help you build pipelines of operations safely.

Summary Table

MethodUse Case
matchFull control, handle all cases
if letCheck for just one variant
unwrap()Crash if error, only for testing
expect()Like unwrap but with custom message
? operatorExit early on error, cleaner syntax
map/and_thenTransform or chain Results

Final Word

Rust uses the Result type for all recoverable errors. It forces you to handle both success and failure clearly. This approach makes your code safe and readable. Use ?, match, and other patterns to write clean error-aware functions.