Skip to Content
Course content

28: Propagating Errors with the ? Operator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

What is the ? operator actually doing under the hood?

If you're coming from a language with try-catch blocks, the ? operator might feel like a magic shortcut for throwing an exception. It isn't. In Rust, it's actually just a very concise way of writing a match statement that returns early.

Imagine you're writing a function to read a username from a local file. Without the ? operator, your code would look something like this:

fn read_username() -> Result<String, std::io::Error> {
    let result = std::fs::read_to_string("username.txt");
    
    let content = match result {
        Ok(text) => text,
        Err(e) => return Err(e), // Return the error early to the caller
    };

    Ok(content)
}

That's a lot of boilerplate for something we do constantly. The ? operator collapses that entire match block into a single character. When you place ? after a Result, it says: "If this is Ok, give me the value inside. If it's Err, return that error from the current function immediately."

fn read_username() -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string("username.txt")?;
    Ok(content)
}

It's much cleaner, right? I personally find it makes the "happy path" of the logic stand out while still being explicit about where things can go wrong.

Why can't I use ? in any function I want?

You've probably already hit a compiler error trying to use ? in main or in a helper function that doesn't return a Result. This is because the ? operator is essentially a return statement in disguise.

Because ? might return an Err, the function it's inside must have a return type that is compatible with that error. You can't use ? in a function that returns () (nothing) because the compiler wouldn't know how to "return" the error to the caller.

If you see an error saying the ? operator can only be used in a function that returns Result or Option, check your function signature. You'll need to change it from something like fn do_work() to fn do_work() -> Result<(), MyError>. Even if you don't have a "successful" value to return, returning Ok(()) at the end of the function satisfies the requirement.

Does this work for Option too, or just Result?

It works for both! I've seen a lot of people assume ? is strictly for error handling, but it's actually defined for both Result<T, E> and Option<T>. When used with an Option, it behaves exactly the same way: if the value is Some(v), it unwraps it; if it's None, it returns None from the whole function.

Here is a quick example of using it to navigate a nested data structure:

struct User {
    profile: Option<Profile>,
}

struct Profile {
    nickname: Option<String>,
}

fn get_nickname(user: User) -> Option<String> {
    // If profile is None, return None. If nickname is None, return None.
    let nickname = user.profile?.nickname?;
    Some(nickname)
}

One critical rule to remember: you cannot mix them in the same function. You can't use ? on a Result and then use ? on an Option in the same block. The return type of the function can only be one or the other. If you need to mix them, you'll have to manually convert the Option into a Result using .ok_or() first.




📋 Practical Task

Exercise: Refactoring the Settings Loader

You have a piece of code that loads a system setting from a file and attempts to parse it into an integer. Currently, it uses verbose match statements. Your task is to refactor the load_setting function to use the ? operator to propagate both I/O errors and parsing errors.

Requirements:

  • The function should return Result<i32, Box<dyn std::error::Error>> to allow for different types of errors (I/O and ParseInt) to be propagated.
  • Replace the match blocks with the ? operator.
  • Ensure the function still returns the parsed integer wrapped in Ok().
use std::fs;

fn load_setting() -> Result<i32, Box<dyn std::error::Error>> {
    // TODO: Refactor the following using the ? operator
    let content_result = fs::read_to_string("setting.txt");
    let content = match content_result {
        Ok(text) => text,
        Err(e) => return Err(Box::new(e)),
    };

    let parsed_result = content.trim().parse<i32>();
    let value = match parsed_result {
        Ok(num) => num,
        Err(e) => return Err(Box::new(e)),
    };

    Ok(value)
}

fn main() {
    match load_setting() {
        Ok(val) => println!("Setting loaded: {}", val),
        Err(e) => eprintln!("Error: {}", e),
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.