LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 1116 min read

Error Handling (Result & Option)

Handle recoverable errors with Result<T, E> and the ? operator, know when panic! is appropriate, and use unwrap/expect responsibly.

Introduction

Rust splits errors into two clear categories: unrecoverable errors, which stop the program via `panic!`, and recoverable errors, which are represented as values using `Result<T, E>` and handled explicitly by the caller. There are no exceptions to catch — error handling is just regular control flow over regular values.

This lesson covers panic!, Result<T, E>, the ? operator for propagating errors concisely, and when unwrap()/expect() are (and aren't) appropriate.

What You Will Learn
  • When to use panic! for unrecoverable errors.
  • How Result<T, E> represents an operation that might succeed or fail.
  • How the ? operator propagates errors up to the caller concisely.
  • When unwrap() and expect() are reasonable, and when they're a production risk.
  • The difference between Option<T> and Result<T, E>.

panic!: Unrecoverable Errors

`panic!` immediately stops the current thread, unwinding (or, depending on configuration, aborting) the program. It's meant for situations that indicate a bug — an invariant your code assumed was true turned out to be false — not for routine, expected failures like a missing file or invalid user input.

Result<T, E>: Recoverable Errors

`Result<T, E>` is an enum with two variants: `Ok(T)` for success, holding the successful value, and `Err(E)` for failure, holding an error value describing what went wrong. Functions that can fail in an expected way — parsing text, reading a file, making a network request — return `Result` instead of panicking, letting the caller decide how to respond.

The ? Operator

Manually matching on every `Result` to propagate errors upward gets verbose fast. The `?` operator does it in one character: if the value is `Ok`, it unwraps it and the expression continues; if it's `Err`, it immediately returns that error from the current function — provided the function's own return type is compatible with that error.

unwrap() and expect()

`.unwrap()` extracts the `Ok`/`Some` value or panics if it was `Err`/`None`. `.expect("message")` does the same but lets you attach a custom panic message, which makes debugging much easier. Both are convenient for prototypes, examples, and tests — but calling them on untrusted or unpredictable input in production code means any unexpected failure crashes the whole program.

Option vs Result

TypeRepresentsUse When
Option<T>A value that may or may not exist (Some/None), with no explanation of why.Absence is a normal, expected possibility — e.g. searching for an item that might not be found.
Result<T, E>An operation that may succeed or fail, with Err(E) carrying a reason.The operation can genuinely fail, and the caller needs to know why.

Code Example

use std::num::ParseIntError;
fn parse_and_double(input: &str) -> Result<i32, ParseIntError> {
let number: i32 = input.parse()?; // ? propagates the error automatically
Ok(number * 2)
}
fn main() {
match parse_and_double("21") {
Ok(value) => println!("Doubled: {}", value),
Err(e) => println!("Failed to parse: {}", e),
}
match parse_and_double("not a number") {
Ok(value) => println!("Doubled: {}", value),
Err(e) => println!("Failed to parse: {}", e),
}
}
Output

Click Run to see what this code prints.

Common Mistakes

Avoid These Mistakes
  • Calling `.unwrap()` or `.expect()` throughout production code and letting the program crash on any unexpected input instead of handling the error gracefully.
  • Using `panic!` for expected, recoverable situations (like a missing file or bad user input) instead of returning a Result.
  • Ignoring a Result entirely — the compiler emits an "unused Result that must be used" warning specifically to catch this class of bug.
  • Overusing the ? operator without giving callers enough context about what actually failed, instead of wrapping errors with useful detail.

Best Practices

  • Return Result<T, E> from any function that can fail in an expected way, and reserve panic! for genuinely unrecoverable bugs.
  • Use the ? operator to propagate errors up to a caller who is better positioned to decide what to do.
  • Reach for .expect("clear message") over a bare .unwrap() so a panic at least explains what went wrong.
  • For larger projects, consider ecosystem crates like anyhow (ergonomic error propagation) or thiserror (defining custom error types) as natural next steps.

Frequently Asked Questions

Option<T> represents a value that may or may not exist (Some/None) with no explanation of why; Result<T, E> represents an operation that may succeed or fail, and Err(E) carries a reason.

If the value is Ok, it unwraps it and continues; if it's Err, it immediately returns that error from the current function, provided the function's return type is compatible.

Not exactly — a Rust panic unwinds (or aborts) the current thread by default and is meant for bugs, not routine error handling, which is Result's job.

Sparingly — it's reasonable when you can prove a value can never be None/Err, or in quick scripts and tests, but production code handling untrusted input should handle errors explicitly.

Key Takeaways

  • panic! is for unrecoverable bugs; Result<T, E> is for expected, recoverable failures.
  • Result has two variants: Ok(T) for success and Err(E) for failure.
  • The ? operator concisely propagates an Err up to the caller.
  • unwrap() and expect() are convenient but risky in production code that handles untrusted input.

Summary

You now know how Rust handles errors as ordinary values instead of exceptions, when to reach for panic! versus Result, and how the ? operator keeps error propagation concise. In the final lesson, you'll pull everything together with tooling, testing, and best practices for writing idiomatic, production-quality Rust.

Next Lesson →

Best Practices & Next Steps