← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Rust Error Handling with Result

rust Beginner
debt(d3/e3/b5/t7)
d3 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'default linter catches the common case' (d3), clippy flags the code_pattern \.unwrap\(\)|\.expect\( and the compiler's #[must_use] warning catches ignored Results, so the common misuse is detected statically rather than silently in production.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3), the quick_fix is to return Result<T, E> and propagate with ?, replacing unwrap/match patterns — a small refactor of a function signature and its call chain rather than a one-line swap.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5), since applies_to spans web, cli, queue-worker, and library contexts and error handling is load-bearing; choosing a concrete error type instead of a unifying enum or Box<dyn Error> slows many work streams and forces propagation rework across the codebase.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7), the misconception is that Rust uses try/catch exceptions and unwrap is the normal way to extract values, contradicting how error handling works in other languages — unwrap silently converts a recoverable error into a process-crashing panic.

About DEBT scoring →

Also Known As

rust result type result and option question mark operator

TL;DR

Rust models recoverable errors as Result<T, E> values returned from functions, not thrown exceptions, with the ? operator for propagation.

Explanation

Rust has no exceptions for recoverable errors. Instead, fallible functions return `Result<T, E>`, an enum with two variants: `Ok(T)` carrying a success value and `Err(E)` carrying an error. Because `Result` is `#[must_use]`, the compiler warns if you ignore it, forcing you to acknowledge that an operation can fail. This makes failure paths visible in type signatures rather than hidden in stack-unwinding control flow.

To extract a value you must handle both variants, typically with `match`, combinators like `map`, `and_then`, `unwrap_or`, or the `?` operator. The `?` operator is the idiomatic propagation tool: in a function returning `Result`, `let f = File::open(path)?;` returns early with the `Err` if the call failed, otherwise unwraps the `Ok` value. `?` also applies the `From` trait to convert the error into the function's declared error type, which is why error-handling crates and custom error enums implement `From` for their sources.

A crucial distinction is `Result` versus `panic!`. `Result` is for expected, recoverable failures - a missing file, invalid input, a network timeout. `panic!` (and the `unwrap`/`expect` methods that trigger it) is for unrecoverable bugs and broken invariants. Reaching for `.unwrap()` everywhere turns recoverable errors into crashes and is a common beginner mistake. `.expect("reason")` is marginally better because it documents the assumption, but production code on real input should propagate or handle, not unwrap.

For error types, libraries commonly use `thiserror` to derive rich enum errors with `Display` and `From` impls, while applications use `anyhow` for a boxed, context-carrying `Result` that is easy to propagate. The `Option<T>` type plays a parallel role for absence (`Some`/`None`) and converts to `Result` via `ok_or`. Combined with `?`, these tools let you write linear, readable code where the happy path dominates and errors flow upward explicitly, giving exhaustive, compiler-checked handling without the invisible control flow of exceptions.

Common Misconception

Rust uses try/catch exceptions like other languages, and .unwrap() is the normal way to get a value out of a Result. In reality errors are ordinary return values, and unwrap turns a recoverable error into a panic that crashes the program.

Why It Matters

Overusing unwrap and expect converts handleable failures into crashes, while ignoring the #[must_use] Result silently drops errors, producing fragile services that panic on the first bad input in production.

Common Mistakes

  • Calling .unwrap() or .expect() on Results from fallible I/O or parsing, turning recoverable errors into runtime panics.
  • Ignoring a returned Result and triggering only an unused-must-use warning instead of handling the failure.
  • Writing nested match blocks to propagate errors when the ? operator would express the same thing in one line.
  • Using panic! or .unwrap() for expected conditions like missing config rather than returning an Err.
  • Returning a concrete error type from one source so ? cannot convert other error types, instead of a unifying enum or Box<dyn Error>.

Avoid When

  • Quick prototypes, examples, or tests where a panic on bad input is acceptable and explicit error types add noise.
  • Cases where an Err genuinely represents an unrecoverable broken invariant, where panic! communicates the bug more honestly.
  • Code where the value is statically guaranteed present, such as a constant literal parse, and expect documents the impossibility.

When To Use

  • Any function performing I/O, parsing, or network calls where failure is an expected runtime condition.
  • Library APIs that should let callers decide how to handle failure rather than crashing their process.
  • Propagating errors up a call stack concisely with ? while converting between error types via From.
  • Building services that must stay alive and respond gracefully when individual requests or inputs are invalid.

Code Examples

✗ Vulnerable
use std::fs::File;
use std::io::Read;

// Returns the parsed number, but panics on every failure path.
fn read_count(path: &str) -> u32 {
    // unwrap panics if the file is missing.
    let mut file = File::open(path).unwrap();
    let mut contents = String::new();
    // unwrap panics on a read error.
    file.read_to_string(&mut contents).unwrap();
    // unwrap panics if the contents are not a valid number.
    contents.trim().parse::<u32>().unwrap()
}

fn main() {
    // A missing file or garbage input crashes the whole program.
    let count = read_count("count.txt");
    println!("{}", count);
}
✓ Fixed
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;

#[derive(Debug)]
enum CountError {
    Io(io::Error),
    Parse(ParseIntError),
}

// From impls let the ? operator convert each source error automatically.
impl From<io::Error> for CountError {
    fn from(e: io::Error) -> Self { CountError::Io(e) }
}
impl From<ParseIntError> for CountError {
    fn from(e: ParseIntError) -> Self { CountError::Parse(e) }
}

// Failure is visible in the signature; ? propagates errors cleanly.
fn read_count(path: &str) -> Result<u32, CountError> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    let count = contents.trim().parse::<u32>()?;
    Ok(count)
}

fn main() {
    match read_count("count.txt") {
        Ok(count) => println!("{}", count),
        Err(e) => eprintln!("could not read count: {:?}", e),
    }
}

Added 5 Jun 2026
Views 7
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 1 ping S 3 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Google 2 Scrapy 1 Amazonbot 1
crawler 4
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Low
⚡ Quick Fix
Return Result<T, E> from fallible functions and propagate with ?, reserving unwrap/expect for genuinely impossible cases or quick prototypes.
📦 Applies To
web cli queue-worker library thiserror anyhow
🔗 Prerequisites
🔍 Detection Hints
\.unwrap\(\)|\.expect\(
Auto-detectable: ✓ Yes clippy
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update

✓ schema.org compliant