---
title: How to Use Result<T, E> in Rust: The Complete Guide
summary: Use Result<T, E> to return success or error values and handle them with match or the ? operator.
lang: en
slug: how-to-use-resultt-e-in-rust-the-complete-guide
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-resultt-e-in-rust-the-complete-guide/
---

# How to Use Result<T, E> in Rust: The Complete Guide



## 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.

```rust
/// 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:

```rust
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.

```rust
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:

```rust
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 `?`.

```rust
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).

```rust
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.

```rust
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.

```rust
// 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

- [How to implement std error Error trait](/en/how-to-implement-std-error-error-trait/)
- [How to Catch Panics in Rust with catch_unwind](/en/how-to-catch-panics-in-rust-with-catchunwind/)
- [How to Handle Errors in Iterator Chains](/en/how-to-handle-errors-in-iterator-chains/)