blog.dopana

Back

Error handling in Rust is unique compared to most mainstream languages. Rust has no try/catch exceptions and no null pointers. Instead, it makes error handling explicit, type-safe, and verifiable at compile time.

Explain Like I’m 10: The Postal Delivery Service#

Think of functions in Rust like ordering a parcel through a postal delivery system:

  1. Successful Delivery (Ok(Package)): The postman arrives, rings your doorbell, and hands you the item.
  2. Recoverable Delivery Issue (Err(Reason)): The package address was slightly wrong or the recipient was not home. The post office sends a notice back to the sender so they can decide what to do (retry, call the customer, or redirect).
  3. Unrecoverable Catastrophe (panic!): The delivery truck broke down completely or the post office building was destroyed. The entire operation halts immediately because there is no safe way to continue.
graph TD
    Op["Operation / Function Call"] --> Choice{"Is the error recoverable?"}
    Choice -->|No: Critical Bug / Invariant Broken| Panic["panic! Macro<br/>(Crash, Unwind Stack, Terminate)"]
    Choice -->|Yes: File Missing, Network Failure, Invalid Input| ResultEnum["Result&lt;T, E&gt;"]
    ResultEnum --> OkBranch["Ok(value) -> Success"]
    ResultEnum --> ErrBranch["Err(error) -> Handle or Propagate with ?"]

Two Categories of Errors in Rust#

FeatureRecoverable Errors (Result<T, E>)Unrecoverable Errors (panic!)
MechanismEnum Result<T, E> (Ok or Err)Macro panic!("...")
Typical CauseFile not found, parse failure, timeoutOut-of-bounds index, broken invariant, assertion fail
Handlingmatch, if let, ?, combinatorsProcess termination or stack unwinding
PhilosophyExpected part of application flowSevere bug; program cannot reliably continue

Unrecoverable Errors with panic!#

When a panic! occurs:

  1. Rust prints the failure message to stderr.
  2. By default, it unwinds the stack — walking back up the stack to clean up resources and memory.
  3. The process exits with a non-zero status code.
src/main.rs
fn main() {
    // Explicit panic
    // panic!("System failure: database connection pool corrupted");

    // Implicit panic from runtime checks
    let numbers = vec![10, 20, 30];
    let _item = numbers[99]; // panics: index out of bounds: the len is 3 but the index is 99
}
rust

[!NOTE] Setting RUST_BACKTRACE=1 in your terminal environment displays the exact call stack leading to where the panic originated.

Recoverable Errors with Result<T, E>#

Rust defines Result in the standard library prelude as:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
rust

Inspecting Result with match#

Unwrapping Shortcuts: unwrap vs expect#

For rapid prototyping or test code, Result provides convenience extraction methods:

src/main.rs
use std::fs::File;

// 1. .unwrap() -> returns value if Ok, panics with generic message if Err
let f1 = File::open("config.json").unwrap();

// 2. .expect(msg) -> returns value if Ok, panics with custom message if Err
let f2 = File::open("config.json").expect("config.json is required to boot the application");
rust

[!TIP] Always prefer .expect() over .unwrap() in production code. A descriptive error message saves hours of debugging in production logs.

Error Propagation with the ? Operator#

Instead of handling every error locally with match, the ? operator unwraps Ok(T) or immediately returns Err(E) from the enclosing function:

sequenceDiagram
    participant Caller as Calling Function
    participant Worker as read_username
    participant FS as File System

    Worker->>FS: File::open("username.txt")
    alt File exists
        FS-->>Worker: Ok(file)
        Worker->>FS: read_to_string()
        alt Read succeeds
            FS-->>Worker: Ok(bytes)
            Worker-->>Caller: Ok(username)
        else Read fails
            FS-->>Worker: Err(io_error)
            Worker-->>Caller: Returns Err(io_error) via ?
        end
    else File missing
        FS-->>Worker: Err(io_error)
        Worker-->>Caller: Returns Err(io_error) via ?
    end

Clean Propagation in Action#

How the ? Operator Works Under the Hood#

The ? operator automatically calls From::from on the error type, converting the specific error into the return error type of the calling function:

graph LR
    Expr["Expression evaluates to Result&lt;T, E1&gt;"] --> Check{"Is it Ok or Err?"}
    Check -->|Ok value| Extract["Unwrap value and continue execution"]
    Check -->|Err e1| Convert["Convert e1 using From::from into E2"]
    Convert --> ReturnEarly["Early return Err(e2) from function"]

Creating Custom Error Types#

In real-world Rust applications, functions may fail with multiple different error kinds (I/O error, JSON parsing error, database error). We group them into an Enum:

Functional Combinators for Result and Option#

Rust provides functional combinators to transform errors smoothly:

  • .map(f): Transforms Ok(v) using f, leaves Err untouched.
  • .and_then(f): Chains another operation returning Result.
  • .unwrap_or(default): Returns inner value or default fallback.
  • .unwrap_or_else(f): Computes fallback value lazily from closure.
src/main.rs
fn get_port_or_default() -> u16 {
    std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8080)
}
rust

Summary#

  • panic! is for bugs and fatal invariant breaches.
  • Result<T, E> is for predictable failures that the caller must decide how to handle.
  • The ? operator provides zero-cost, clean error propagation and automatic type conversion.
  • Custom enum errors unify diverse error types under a single robust interface.

References#