How to Use Result<T, E> in Rust

The Complete Guide

Use Result<T, E> to return success or error values and handle them with match or the ? operator.

The envelope that never lies

You write a function to parse a configuration file. In Python, you call json.load() and wrap it in a try block, hoping you caught every exception type. In JavaScript, you check if the result is null and pray the documentation was honest about when null appears. Rust takes a different path. The function signature itself declares exactly what can go wrong. You cannot ignore the possibility of failure. The compiler forces you to make a choice: handle the error now, or pass it up to the caller.

This is Result<T, E>. It is the backbone of error handling in Rust. Every fallible operation returns a Result. The type parameters T and E stand for the success value and the error value. The compiler guarantees that you will always get one or the other. You never get a null pointer. You never get a silent crash. You get a value wrapped in a container that tells you exactly what happened.

The sealed envelope analogy

Think of Result like a sealed envelope from a government agency. You submit a form and receive an envelope in return. The envelope does not contain your passport directly. It contains either the passport or a rejection letter explaining why the application failed.

You cannot use the envelope as a passport. You must open it. If you find the passport, you proceed. If you find the rejection letter, you must decide whether to appeal or give up. Rust wraps every fallible operation in this envelope. The type Result<T, E> means "I will give you a value of type T, or I will give you an error of type E." You get one or the other. Never both. Never neither.

This explicitness eliminates a whole class of bugs. In languages with exceptions, control flow can jump unpredictably. An exception can be thrown deep in a call stack and caught far away, making it hard to trace where the failure originated. With Result, the error is just data. It flows through the program like any other value. You can see exactly where errors are handled and where they propagate. The control flow is visible in the code structure.

Minimal example: parsing a string

The simplest way to see Result in action is string parsing. Converting a string to a number can fail if the string contains invalid characters.

/// Demonstrates basic Result handling with match.
fn main() {
    // parse returns Result<i32, ParseIntError>.
    // The compiler knows this type exists and enforces handling.
    let result = "42".parse::<i32>();

    // match forces you to handle both Ok and Err variants.
    // You cannot access the inner value without this exhaustive check.
    match result {
        Ok(value) => println!("Parsed successfully: {}", value),
        Err(error) => eprintln!("Parsing failed: {}", error),
    }
}

The match expression checks which variant the Result holds. If it is Ok, the value is extracted into value. If it is Err, the error is extracted into error. The compiler checks that you have covered all variants. If you forget the Err arm, the code does not compile. This prevents accidental error suppression.

Anatomy of Result

Result is defined in the standard library as an enum:

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

This is just data. At runtime, a Result is a small struct containing a discriminant (a tag indicating Ok or Err) and the payload. There is no magic. No hidden exception table. No vtable lookup. The code branches based on the discriminant. This makes performance predictable. The compiler can often optimize the match away entirely, leaving just the fast path when the success case is known.

The type parameters T and E can be any types. In practice, T is the success value you want, and E is an error type that implements the std::error::Error trait. The community convention is to use specific error types rather than generic strings. This allows callers to inspect the error and react appropriately. For example, a database error might distinguish between "connection refused" and "invalid query," allowing the application to retry the connection or fix the query.

The ? operator and linear flow

Writing match for every operation quickly becomes verbose. Rust provides the ? operator to reduce boilerplate. The ? operator unwraps the Ok value or propagates the Err up the call stack.

use std::fs;
use std::io;

/// Reads a file and returns its contents.
/// Propagates io::Error if reading fails.
fn read_file_contents(path: &str) -> Result<String, io::Error> {
    // fs::read_to_string returns Result<String, io::Error>.
    // The ? operator extracts the String on Ok.
    // On Err, it returns the error immediately from this function.
    let contents = fs::read_to_string(path)?;

    Ok(contents)
}

The ? operator is syntactic sugar for a match. It expands to:

let contents = match fs::read_to_string(path) {
    Ok(v) => v,
    Err(e) => return Err(e.into()),
};

If the result is Ok, the value is extracted. If it is Err, the error is converted using .into() and returned from the current function. This allows you to write fallible code in a linear style. You chain operations with ? and let errors bubble up. The function returns as soon as the first error occurs.

This pattern is the standard way to handle errors in Rust. It keeps the success path clean and readable. Error handling is pushed to the boundaries of the system, where you have enough context to decide what to do.

Realistic example: composing operations

Real code often chains multiple fallible operations. Each operation might return a different error type. You need to convert errors to a common type to use ?.

use std::fs;
use std::io;

/// Reads a config file and parses the first line as an integer.
/// Returns the number or an io::Error if reading or parsing fails.
fn read_config_number(path: &str) -> Result<i32, io::Error> {
    // fs::read_to_string returns Result<String, io::Error>.
    // The error type matches the function return, so ? works directly.
    let content = fs::read_to_string(path)?;

    // parse returns Result<i32, ParseIntError>.
    // The error type does not match io::Error.
    // map_err converts ParseIntError to io::Error.
    let number = content.trim().parse::<i32>()
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    Ok(number)
}

The map_err method transforms the error inside the Result. It takes a closure that converts the original error to a new type. Here, we wrap the ParseIntError in an io::Error with the InvalidData kind. This allows us to use ? to propagate the error.

The community convention for custom error types is to use the thiserror crate. It generates the Error trait implementation and error conversion logic for you. Writing impl std::error::Error by hand is rare in production code. thiserror lets you define error variants and derive conversions automatically.

Pitfalls and compiler errors

Assigning Result to a value

You cannot assign a Result directly to a variable of the inner type. The compiler rejects this with E0308 (mismatched types).

fn main() {
    // This fails to compile.
    // Result<i32, _> is not the same as i32.
    let number: i32 = "42".parse();
}

You must unwrap the result using match, ?, or an explicit method like unwrap(). The compiler forces you to acknowledge the possibility of failure.

Type mismatch with ?

The ? operator requires the error types to match. If your function returns Result<T, IoError> and you call a function returning Result<T, ParseError>, the ? fails. You get E0277 (trait bound not satisfied) or a type mismatch error.

use std::io;

/// Returns Result<i32, io::Error>.
fn parse_and_read() -> Result<i32, io::Error> {
    // parse returns Result<i32, ParseIntError>.
    // ? tries to convert ParseIntError to io::Error.
    // Conversion fails because ParseIntError does not implement Into<io::Error>.
    let number = "42".parse::<i32>()?;

    Ok(number)
}

You must convert the error using .map_err() or .into() if a conversion exists. The From trait enables automatic conversion with ?. If From<ParseError> is implemented for IoError, then ? works without explicit conversion. This is why error types often implement From for lower-level errors.

Unwrap abuse

Calling .unwrap() on a Result is a promise to the compiler that you know the value is Ok. If it is Err, your program panics. This is the Rust equivalent of assert. Use it in tests or main where you control the input. Never use it in library code that handles user data.

The convention is to use expect() instead of unwrap(). expect() takes a message that explains why the value should be Ok. This message appears in the panic output, making debugging easier.

// Good: expect with a clear message.
let config = std::env::var("DATABASE_URL")
    .expect("DATABASE_URL must be set for this application");

// Bad: unwrap with no context.
let config = std::env::var("DATABASE_URL").unwrap();

Treat unwrap as a contract violation. If it panics, your code lied about the precondition.

Decision matrix

Use match when you need to handle the error case with specific logic, like logging, retrying, or transforming the error before returning.

Use ? when you want to propagate the error up the call stack. This is the standard way to chain fallible operations without nesting.

Use expect when the error indicates a programming bug or an invariant violation, and you want a clear panic message explaining why the code failed.

Use unwrap only in tests or main functions where the input is controlled and a failure means the test setup is broken.

Use if let Ok(value) = result when you only care about the success case and want to ignore the error silently. This is a signal to readers that the failure is acceptable.

Use map_err when you need to convert an error type to match the function's return signature, usually to wrap a lower-level error in a higher-level context.

Use Result::and_then when you need to chain operations that return Result without creating intermediate bindings. This is useful for composing small transformations.

Where to go next