Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Error Handling Practice: Result, Option, ? Operator
Rust40 exercises

Rust Error Handling Practice: Result, Option, ? Operator

Master Rust error handling with Result, Option, and the ? operator. Practice error propagation, custom error types, and idiomatic patterns.

Cheat SheetCommon ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write a function signature for parse_number that takes a &str and returns Result<i32, String>.

On this page
  1. 1Compiler Error E0277: ? Can Only Be Used in a Function That Returns Result or Option
  2. 2The ? Operator: Clean Error Propagation
  3. 3? with Option
  4. 4Compiler Error E0277: ? Couldn't Convert the Error
  5. 5Adding Context to Errors
  6. 6ok_or vs ok_or_else: Option → Result
  7. 7Result Combinators Cheat Sheet
  8. 8When unwrap and expect Are Appropriate
  9. 9Custom Error Types
  10. 10Further Reading
Compiler Error E0277: ? Can Only Be Used in a Function That Returns Result or OptionThe ? Operator: Clean Error Propagation? with OptionCompiler Error E0277: ? Couldn't Convert the ErrorAdding Context to Errorsok_or vs ok_or_else: Option → ResultResult Combinators Cheat SheetWhen unwrap and expect Are AppropriateCustom Error TypesFurther Reading

Rust has no exceptions. Instead, it uses types: Result<T, E> for operations that can fail, Option<T> for values that might be absent. The ? operator makes error propagation clean and concise.

This approach forces you to handle errors explicitly. No silent failures, no unexpected crashes. But the syntax takes practice: When do you use ? vs match? What's the difference between unwrap and expect? How do you chain fallible operations?

This page focuses on error handling patterns until they become automatic. From basic Result matching to the ? operator and custom error types, you'll practice idiomatic Rust error handling.

Related Rust Topics
Rust Structs & Enums: Custom Types, Option & ResultRust Pattern Matching: match, if let, Destructuring

The most common first error. You used ? in a function that returns ():

// WRONG: main returns () but uses ?
// fn main() {
//     let config = std::fs::read_to_string("config.toml")?;  // error[E0277]
// }

// RIGHT: return Result from main
fn main() -> Result<(), std::io::Error> {
    let config = std::fs::read_to_string("config.toml")?;
    println!("{config}");
    Ok(())
}

For application entry points, Box<dyn std::error::Error> accepts any error type:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;
    let port: u16 = config.trim().parse()?;  // different error type — still works
    Ok(())
}

Ready to practice?

Start practicing Rust Error Handling: Result, Option, ? Operator with spaced repetition

? unwraps Ok/Some and returns early with Err/None. It replaces verbose match blocks. The function must return a compatible Result or Option.

? unwraps Ok/Some and returns early with Err/None. It replaces verbose match blocks:

// Without ?: verbose match
fn read_username() -> Result<String, std::io::Error> {
    let f = std::fs::read_to_string("username.txt");
    let username = match f {
        Ok(s) => s,
        Err(e) => return Err(e),
    };
    Ok(username.trim().to_string())
}

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

? also works with Option — returns None early if the value is absent:

fn first_word(s: &str) -> Option<&str> {
    let end = s.find(' ')?;  // None if no space found
    Some(&s[..end])
}

fn longest_line(text: &str) -> Option<&str> {
    let mut longest: Option<&str> = None;
    for line in text.lines() {
        let current = longest.unwrap_or("");
        if line.len() > current.len() {
            longest = Some(line);
        }
    }
    longest
}

You cannot mix ? on Result and Option in the same function. Convert with .ok() (Result → Option) or .ok_or() (Option → Result). If you're coming from Python, the approach to errors is fundamentally different; see Python vs Rust error handling compared for a side-by-side breakdown.

When ? produces a different error type than your function returns, the compiler rejects it:

// WRONG: io::Error vs ParseIntError
// fn load_port() -> Result<u16, std::io::Error> {
//     let s = std::fs::read_to_string("port.txt")?;  // io::Error — OK
//     let port = s.trim().parse::<u16>()?;             // ParseIntError — error[E0277]
//     Ok(port)
// }

// RIGHT Option 1: map_err to convert inline
fn load_port() -> Result<u16, String> {
    let s = std::fs::read_to_string("port.txt")
        .map_err(|e| format!("read failed: {e}"))?;
    let port = s.trim().parse::<u16>()
        .map_err(|e| format!("bad port: {e}"))?;
    Ok(port)
}

// RIGHT Option 2: Box<dyn Error> accepts any error type
fn load_port_boxed() -> Result<u16, Box<dyn std::error::Error>> {
    let s = std::fs::read_to_string("port.txt")?;
    let port = s.trim().parse::<u16>()?;
    Ok(port)
}

Chained ? calls lose context about which step failed. Use .map_err(|e| format!("failed to parse config: {e}")) to add context at each step. For application code, the anyhow crate provides .context().

Chained ? calls propagate the inner error as-is, losing where the failure happened. Add context with map_err:

fn load_config() -> Result<Config, String> {
    let text = std::fs::read_to_string("config.toml")
        .map_err(|e| format!("failed to read config: {e}"))?;
    let port: u16 = text.lines().next().unwrap_or("0")
        .parse()
        .map_err(|e| format!("failed to parse port: {e}"))?;
    Ok(Config { port })
}

For application code, the anyhow crate provides .context() which is more ergonomic. For libraries, define concrete error types so callers can match on variants. For a printable reference of all these combinators, see the error handling cheat sheet.

Both convert Option to Result, but differ in when the error value is created:

let name: Option<&str> = Some("Alice");

// ok_or: error value computed eagerly (always created)
let result = name.ok_or("name is required");

// ok_or_else: error value computed lazily (only on None)
let result = name.ok_or_else(|| format!("name missing at line {}", line_num));

Use ok_or for cheap static errors. Use ok_or_else when the error involves allocation or computation.

Avoid match spam with these combinators:

CombinatorDoesExample
mapTransform Ok valueresult.map(|x| x * 2)
map_errTransform Err valueresult.map_err(|e| format!("{e}"))
and_thenChain fallible operationsresult.and_then(|x| validate(x))
or_elseTry fallback on Errresult.or_else(|_| try_backup())
unwrap_orDefault on Errresult.unwrap_or(0)
unwrap_or_elseLazy default on Errresult.unwrap_or_else(|e| handle(e))
okResult → Option (drop error)result.ok()
inspect_errLog error without consumingresult.inspect_err(|e| eprintln!("{e}"))

unwrap() is fine in tests and prototyping. Use expect("reason") when you can prove the call never fails. In production, prefer ? for propagation—panic! is for bugs, not bad user input.

unwrap() and expect() both panic on error. They are not always wrong:

  • Tests: .unwrap() is fine — a panic is a test failure
  • Prototyping: .unwrap() keeps code short while exploring
  • Provably safe: .expect("reason") when you can prove the call never fails
// Fine: "1" is always a valid u32
let one: u32 = "1".parse().expect("literal '1' is always valid");

// Fine in tests
#[test]
fn test_parse() {
    let result = parse_config("valid input").unwrap();
    assert_eq!(result.port, 8080);
}

In production, prefer ? for propagation. panic! is for bugs and impossible states, not for bad user input.

For libraries or complex applications, define an error enum:

use std::fmt;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Config(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {e}"),
            AppError::Parse(e) => write!(f, "parse error: {e}"),
            AppError::Config(msg) => write!(f, "config error: {msg}"),
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

// Now ? converts both error types automatically
fn load_port() -> Result<u16, AppError> {
    let s = std::fs::read_to_string("port.txt")?;
    let port = s.trim().parse::<u16>()?;
    Ok(port)
}

The thiserror crate generates Display and From impls from attributes, reducing this boilerplate significantly. Custom error enums with From impls are a common pattern in Rust; see structs and enums for the underlying enum definition syntax.

  • The Rust Book: Error Handling — panic vs recoverable errors
  • The Rust Book: Propagating Errors — the ? operator
  • std::result::Result — full combinator API
  • std::option::Option — ok_or, ok_or_else, and more

When to Use Rust Error Handling: Result, Option, ? Operator

  • Use `Result` for operations that can fail in expected ways.
  • Use `Option` for values that might be absent (not errors, just optional).
  • Use `?` to propagate errors up the call stack cleanly.
  • Use `unwrap()` and `expect()` only when failure is truly impossible or you want to panic.
  • Define custom error types for libraries or when you need rich error context.

Check Your Understanding: Rust Error Handling: Result, Option, ? Operator

Prompt

How does the ? operator work and when should you use it?

What a strong answer looks like

`?` unwraps a Result/Option if Ok/Some, otherwise returns early with the error. Use it to propagate errors without explicit match. The function must return a compatible Result/Option.

What You'll Practice: Rust Error Handling: Result, Option, ? Operator

Create and match on ResultCreate and match on OptionUse the ? operator for propagationKnow when to use unwrap, expect, and ?Chain fallible operationsConvert between error types with map_errUse ok_or to convert Option to ResultUnderstand panic vs recoverable errorsDefine custom error types

Common Rust Error Handling: Result, Option, ? Operator Pitfalls

  • You get `thread panicked at 'called unwrap() on an Err value'` -- you used `.unwrap()` in code that can actually fail. Replace with `?` to propagate or `match` to handle the error.
  • You get `error[E0277]: the ? operator can only be used in a function that returns Result or Option` -- your function returns `()`. Change the signature to return `Result<(), Error>` so `?` has somewhere to propagate.
  • You silently discard errors with `let _ = fallible_op()` and bugs go unnoticed -- the `_` pattern suppresses the unused-Result warning. At minimum log the error, or propagate with `?`.
  • You use `panic!` for a condition the caller could recover from -- `panic!` is for bugs and impossible states, not for bad user input. Return `Err(...)` so callers can decide what to do.
  • You write `.expect("something failed")` and the panic message is useless -- the string in `expect` should explain the assumption, not describe the symptom. Write `.expect("config.toml must exist in the working directory")`.
  • You chain five `?` calls and the error message gives no context about which step failed -- each `?` propagates the inner error as-is. Use `.map_err(|e| format!("failed to parse config: {e}"))` to add context at each step.

Rust Error Handling: Result, Option, ? Operator FAQ

What is the difference between unwrap and expect?

Both panic on error, but `expect("message")` includes your message in the panic output. Use `expect` when you're sure the operation should succeed. The message documents why.

When should I use ? vs match?

Use `?` when you want to propagate the error up. Use `match` when you want to handle the error locally or transform it. `?` is for "let the caller deal with it"; `match` is for "I'll handle it here".

How do I convert between error types?

Use `map_err(|e| ...)` to transform errors. Many errors implement `From<OtherError>`, so `?` can convert automatically. The `anyhow` and `thiserror` crates help with complex error handling.

What is the difference between Result and Option?

`Option` is for absence (might not exist). `Result` is for failure (something went wrong). `None` means "nothing"; `Err(e)` means "error with details".

Can I use ? in main()?

Yes! Declare main as `fn main() -> Result<(), Error>`. Return `Ok(())` for success. Errors print and return exit code 1.

Rust Error Handling: Result, Option, ? Operator Syntax Quick Reference

Result basics
fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}
Match on Result
match divide(10, 2) {
    Ok(result) => println!("Result: {result}"),
    Err(e) => println!("Error: {e}"),
}
? operator
fn read_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("file.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
unwrap and expect
let x = some_result.unwrap();     // panics on Err
let x = some_result.expect("should never fail");
unwrap_or default
let x = result.unwrap_or(0);      // default on Err
let x = result.unwrap_or_else(|| compute_default());
map_err
let result = op().map_err(|e| format!("operation failed: {e}"))?;
Option to Result
let x = optional.ok_or("value was None")?;
Chain with and_then
let result = op1()
    .and_then(|x| op2(x))
    .and_then(|y| op3(y));
main with Result
fn main() -> Result<(), Box<dyn Error>> {
    let config = read_config()?;
    run(config)?;
    Ok(())
}

Rust Error Handling: Result, Option, ? Operator Sample Exercises

Example 1Difficulty: 2/5

Fill in the Result variant for the error case.

Err
Example 2Difficulty: 1/5

Fill in to create a successful Result containing the value 42.

Ok(42)
Example 3Difficulty: 2/5

Fill in the method to unwrap the Ok value.

unwrap

+ 37 more exercises

Quick Reference
Rust Error Handling: Result, Option, ? Operator Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Also in Other Languages

Python Exception Handling Practice: try/except/else/finally, raise from, custom exceptionsJavaScript Error Handling Practice

Start practicing Rust Error Handling: Result, Option, ? Operator

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.