Web Analytics

Result Type

Intermediate ~35 min read

The Result<T, E> type is Rust's way of handling recoverable errors. Unlike panics, which stop execution, Result allows you to return either a success value (Ok(T)) or an error value (Err(E)), letting the caller decide how to handle the error. This is the preferred way to handle errors in Rust.

What is Result?

Result<T, E> is an enum with two variants:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • Ok(T) - Contains the success value of type T
  • Err(E) - Contains the error value of type E
In the Prelude: Result is so commonly used that it's included in the prelude - you don't need to import it!

Creating Results

You can create Result values directly:

let success: Result<i32, &str> = Ok(42);
let failure: Result<i32, &str> = Err("something went wrong");

Functions That Return Result

Many standard library functions return Result. For example, file operations:

use std::fs::File;

let file_result = File::open("hello.txt");
// file_result is Result<File, std::io::Error>

You can also write functions that return Result:

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}
Output
Click Run to execute your code

Handling Results with match

The most explicit way to handle a Result is with match:

let result = divide(10, 2);

match result {
    Ok(value) => println!("Result: {}", value),
    Err(error) => println!("Error: {}", error),
}
Exhaustive Matching: match ensures you handle both Ok and Err cases. The compiler will error if you miss one!

Handling Different Error Types

When working with I/O operations, you can match on different error kinds:

use std::fs::File;
use std::io::ErrorKind;

let file_result = File::open("hello.txt");

let file = match file_result {
    Ok(file) => file,
    Err(error) => match error.kind() {
        ErrorKind::NotFound => {
            println!("File not found");
            // Handle file not found
            return;
        }
        ErrorKind::PermissionDenied => {
            println!("Permission denied");
            return;
        }
        other_error => {
            println!("Other error: {:?}", other_error);
            return;
        }
    },
};

Result Helper Methods

Result provides many useful methods for handling errors:

unwrap_or() - Provide Default Value

let result: Result<i32, &str> = Err("error");
let value = result.unwrap_or(0);  // Returns 0 if Err

unwrap_or_else() - Compute Default

let value = result.unwrap_or_else(|error| {
    println!("Error: {}", error);
    0  // Compute default value
});

map() - Transform Ok Value

let result: Result<i32, &str> = Ok(5);
let doubled = result.map(|x| x * 2);  // Ok(10)

map_err() - Transform Err Value

let result: Result<i32, &str> = Err("error");
let mapped = result.map_err(|e| format!("Error: {}", e));

and_then() - Chain Results

let result: Result<i32, &str> = Ok(5);
let chained = result.and_then(|x| {
    if x > 0 {
        Ok(x * 2)
    } else {
        Err("must be positive")
    }
});

or_else() - Handle Error Case

let result: Result<i32, &str> = Err("error");
let recovered = result.or_else(|e| {
    println!("Recovering from: {}", e);
    Ok(0)  // Return alternative Result
});

is_ok() and is_err() - Check Without Unwrapping

if result.is_ok() {
    println!("Success!");
}

if result.is_err() {
    println!("Error occurred");
}
Output
Click Run to execute your code
Avoid unwrap() and expect(): While unwrap() and expect() exist on Result, they cause panics. Use them only in examples, tests, or when you're absolutely certain the value is Ok.

Using if let

When you only care about one variant, if let is more concise:

if let Ok(value) = result {
    println!("Success: {}", value);
}

if let Err(error) = result {
    println!("Error: {}", error);
}

Chaining Results

You can chain multiple operations that return Result using and_then():

fn parse_number(s: &str) -> Result<i32, String> {
    s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}

fn double(n: i32) -> Result<i32, String> {
    Ok(n * 2)
}

let result = parse_number("5")
    .and_then(double)
    .and_then(|n| Ok(n + 1));

match result {
    Ok(value) => println!("Result: {}", value),
    Err(e) => println!("Error: {}", e),
}
Method Chaining: and_then() allows you to chain operations that return Result, making error handling more elegant and readable.

When to Use Result

Use Result<T, E> when:

  • Errors are recoverable: The caller can handle the error
  • I/O operations: File operations, network requests, etc.
  • Parsing/validation: User input, data parsing, etc.
  • Production code: When you need robust error handling
  • Library functions: When callers need to handle errors

Use panic! when:

  • Unrecoverable errors: When there's no way to recover
  • Programming errors: Bugs in your code
  • Examples/tests: When panics are acceptable

Best Practices

  • Always handle both cases: Use match or if let to handle Ok and Err
  • Use helper methods: unwrap_or, map, and_then make code cleaner
  • Chain operations: Use and_then() to chain Result-returning functions
  • Provide context: Use map_err() to add context to errors
  • Avoid unwrap() in production: Always handle errors properly
  • Use ? operator: For error propagation (covered in next lesson)

Common Mistakes

1. Forgetting to handle the Err case

// Wrong - Only handles Ok
let result: Result<i32, &str> = Err("error");
let value = result.unwrap();  // Panics!

// Correct - Handle both cases
match result {
    Ok(value) => println!("{}", value),
    Err(error) => println!("Error: {}", error),
}

2. Using unwrap() everywhere

// Wrong - Can panic
let file = File::open("file.txt").unwrap();

// Correct - Handle the error
let file = match File::open("file.txt") {
    Ok(file) => file,
    Err(error) => {
        println!("Failed to open file: {}", error);
        return;
    }
};

3. Not providing error context

// Wrong - Generic error
fn parse(s: &str) -> Result<i32, &str> {
    s.parse().map_err(|_| "error")
}

// Correct - Descriptive error
fn parse(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| format!("Failed to parse '{}': {}", s, e))
}

4. Not chaining Results properly

// Wrong - Nested matches
let result1 = parse_number("5");
match result1 {
    Ok(n) => {
        let result2 = double(n);
        match result2 {
            Ok(v) => println!("{}", v),
            Err(e) => println!("{}", e),
        }
    }
    Err(e) => println!("{}", e),
}

// Correct - Use and_then
parse_number("5")
    .and_then(double)
    .map(|v| println!("{}", v))
    .map_err(|e| println!("{}", e));

Exercise: Result Handling Practice

Task: Implement functions that return Result and handle errors properly.

Requirements:

  • Write a function to parse numbers from strings
  • Write a safe division function
  • Chain operations using and_then()
  • Handle all error cases properly
Output
Click Run to execute your code
Show Solution
fn parse_number(s: &str) -> Result<i32, String> {
    s.parse::<i32>()
        .map_err(|e| format!("Failed to parse '{}': {}", s, e))
}

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

fn parse_and_divide(s: &str) -> Result<i32, String> {
    parse_number(s)
        .and_then(|n| {
            if n == 0 {
                Err(String::from("Cannot divide by zero"))
            } else {
                safe_divide(100.0, n as f64)
                    .map(|result| result as i32)
            }
        })
}

Summary

  • Result<T, E> represents success (Ok) or failure (Err)
  • Use match to handle both cases exhaustively
  • Use helper methods: unwrap_or(), map(), and_then()
  • Chain operations with and_then() for cleaner code
  • Use map_err() to add context to errors
  • Result is for recoverable errors, panic is for unrecoverable
  • Always handle both Ok and Err cases
  • Avoid unwrap() in production code
  • Many standard library functions return Result

What's Next?

Now that you understand Result, you'll learn about error propagation using the ? operator. This powerful operator allows you to propagate errors up the call stack automatically, making error handling much more concise and readable.