---
title: How to handle errors in tests
summary: Handle test errors by using unwrap() or expect() to panic immediately on failure.
lang: en
slug: how-to-handle-errors-in-tests
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-handle-errors-in-tests/
---

# How to handle errors in tests



## The silent failure trap

You write a function that parses a configuration file. It returns `Result<Config, Error>`. You write a test to verify the parsing logic. You call the function, assign the result to a variable, and write an assertion on the config value. You forget to handle the `Result`. The test passes. You deploy. The production code crashes because the config was invalid, but your test suite gave you a green checkmark.

Or consider the opposite. You use `unwrap()` everywhere. A test fails. The output shows a stack trace pointing to line 42 of `std::result`, with the message "called `Result::unwrap()` on an `Err` value". You have twelve `unwrap()` calls in that test. You spend ten minutes hunting down which one failed.

Tests need to fail loudly and tell you exactly what broke. Rust gives you precise tools to turn errors into test failures, but you have to choose the right tool for the context. The goal is fast failure with maximum signal.

## Turning results into failures

In Rust, functions that can fail return `Result<T, E>` or `Option<T>`. These types wrap the success value or the error. In production code, you handle these gracefully. You return errors, you log them, you provide fallbacks.

In tests, you usually want the opposite. You want the test to crash immediately if something goes wrong. A crashing test is a failed test. The test runner catches the crash and reports failure. `unwrap()` and `expect()` are the tools that turn a wrapped result into a hard crash on error. They are the smoke alarms of your test suite. If the result is an error, the alarm screams, and the test fails.

This approach enforces a strict invariant: the test setup must succeed. If the setup fails, the test cannot proceed, and the failure is visible. This prevents the silent failure trap where a test passes because it ignored an error.

## Minimal examples: unwrap versus expect

The most common way to handle a `Result` in a test is to extract the value and assert on it. `unwrap()` does this. If the `Result` is `Ok`, you get the value. If it is `Err`, the test panics.

`expect()` works identically but accepts a message. That message appears in the panic output. This is crucial for debugging. The message acts as a comment that the compiler enforces. If the test fails, the message tells you what went wrong without forcing you to grep the source code.

```rust
/// Parses a config string into a Config struct.
fn parse_config(input: &str) -> Result<Config, &'static str> {
    if input.is_empty() {
        Err("Empty config")
    } else {
        Ok(Config { port: 8080 })
    }
}

struct Config { port: u16 }

#[test]
fn test_parse_success() {
    // unwrap() extracts the value if Ok.
    // If the Result is Err, the test panics immediately.
    // This is the standard way to assert success in simple tests.
    let config = parse_config("valid.conf").unwrap();
    assert_eq!(config.port, 8080);
}

#[test]
fn test_parse_with_context() {
    // expect() works like unwrap() but includes a custom message.
    // Use this when the failure reason isn't obvious from the function name.
    // The message helps identify the failure point in complex tests.
    let config = parse_config("valid.conf")
        .expect("Parsing 'valid.conf' should always succeed");
    assert_eq!(config.port, 8080);
}
```

Community convention favors `expect()` over `unwrap()` in most test code. The message documents the invariant. It tells future readers why this call should succeed. If the test fails, the message saves debugging time. Treat `expect()` as a contract. The message is the reason the contract holds.

## What happens at runtime

When the test runs, `parse_config` executes. If it returns `Ok(Config)`, `unwrap()` sees the `Ok` variant and returns the `Config`. The assertion runs. If the assertion passes, the test passes.

If `parse_config` returns `Err`, `unwrap()` calls `panic!`. The panic bubbles up the call stack. The test harness catches the panic. The test is marked as failed. The output shows the panic message. With `expect()`, the message you provided is included in the panic output.

This flow ensures fast failure. The test stops at the first error. You don't get cascading failures from subsequent assertions that depend on the failed setup. This keeps test output clean and actionable.

## Tests that return Result

Rust tests can return a `Result`. This is a powerful feature that many beginners miss. If a test function returns `Result<(), E>`, the test passes if the function returns `Ok(())`. The test fails if the function returns `Err`.

This allows you to use the `?` operator inside tests. The `?` operator propagates errors. If a call fails, the `?` returns the error from the test function. The test runner sees the error and fails the test. This keeps the error flow clean. You avoid nested `unwrap()` calls. You avoid deep indentation from `match` statements.

```rust
use std::error::Error;

#[test]
fn test_with_result_return() -> Result<(), Box<dyn Error>> {
    // The ? operator propagates errors.
    // If parse_config returns Err, the test fails immediately.
    // The error message is included in the test output.
    let config = parse_config("valid.conf")?;
    
    // Additional setup can also use ?.
    let data = load_data(&config)?;
    
    assert_eq!(data.items.len(), 5);
    Ok(())
}
```

This pattern shines in tests with multiple fallible steps. Each step can fail cleanly. The test output shows the error from the first failure. You don't need `expect()` messages for every call. The error type provides the context. This reduces boilerplate and keeps the test logic focused on assertions.

Convention aside: returning `Result` from tests is idiomatic in larger codebases. It scales better than `unwrap()` chains. Use it when the test has more than two or three fallible operations.

## Asserting on the Result directly

Sometimes you don't want to unwrap. You want to verify the `Result` itself. `assert_eq!` compares values. You can compare a `Result` against an expected `Ok` or `Err`.

This approach has a distinct advantage. If the assertion fails, you get a diff. The diff shows the actual `Result` versus the expected `Result`. This is helpful when the error type is complex. You can see exactly which variant failed or which field differed.

```rust
#[test]
fn test_assert_result() {
    let result = parse_config("valid.conf");
    
    // assert_eq! compares the whole Result.
    // If it fails, you get a diff showing Ok vs Err or value mismatch.
    // This is useful for inspecting the exact failure mode.
    assert_eq!(result, Ok(Config { port: 8080 }));
}
```

This pattern is also useful for testing error cases. You can assert that a function returns a specific error. You don't need `unwrap_err()`. You just compare against the expected error.

```rust
#[test]
fn test_parse_empty_fails() {
    let result = parse_config("");
    
    // Asserting on the error directly verifies the failure mode.
    // This is clearer than checking is_err() and then unwrapping.
    assert_eq!(result, Err("Empty config"));
}
```

Use this when you need to inspect the error details or when the error type derives `PartialEq`. It keeps the test declarative. You state what you expect, and the assertion framework checks it.

## Testing panics

Not all failures are `Result` errors. Some code panics on invalid input. Index out of bounds, arithmetic overflow, or explicit `panic!` calls. You need to verify that panics occur when expected.

The `#[should_panic]` attribute marks a test as expected to panic. The test passes if the code panics. The test fails if the code completes without panicking.

You can add an `expected` string to `#[should_panic]`. The test passes only if the panic message contains that string. This prevents false positives. A different panic in the code won't accidentally pass the test.

```rust
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_vector_panic() {
    let v = vec![1, 2, 3];
    
    // This access triggers a panic.
    // The test passes only if the panic message contains the expected string.
    // Without the expected string, any panic would pass the test.
    let _x = v[10];
}
```

Always use the `expected` string. It makes the test precise. It documents the expected panic message. It prevents the test from passing due to unrelated panics.

## Pitfalls and compiler errors

The silent killer is ignoring the result. If you write `let result = parse_config("bad");` and do nothing with `result`, the test passes. The compiler might warn about unused variables, but it won't fail the test. Always assert or unwrap. If you intentionally ignore a result, use `let _ = result;` to signal the decision. But in tests, ignoring results is almost always a bug.

Another pitfall is `unwrap()` noise. If you have many `unwrap()` calls, a failure gives you little context. The stack trace points to the panic, but you have to search the code to find the call. `expect()` solves this. Use `expect()` when the call isn't self-documenting.

Compiler errors can help you catch mistakes. If you forget to unwrap and pass a `Result` to a function expecting `Config`, you get `E0308` (mismatched types). The compiler tells you `Result<Config, Error>` is not `Config`. This is good. It forces you to handle the error. Don't suppress this error with type coercion or ignored variables. Let the compiler enforce correctness.

Be careful with `#[should_panic]` without an expected string. It passes on any panic. A typo in the test code could cause a panic, and the test would pass. This hides bugs. Always specify the expected message.

## Decision matrix

Choose the error handling strategy based on the test complexity and the need for context.

Use `unwrap()` when the error case is impossible in the test setup and the function name makes the failure obvious.

Use `expect()` when the test setup is complex or the function is generic, so a custom message helps identify the failure point.

Use `assert_eq!(result, Ok(...))` when you need to verify the exact result value and benefit from the diff output on failure.

Use `-> Result` in the test signature when the test has multiple fallible steps and you want to use the `?` operator for clean error propagation.

Use `#[should_panic(expected = "...")]` when you are testing code that panics on invalid input, like index out of bounds or arithmetic overflow.

Use `unwrap_err()` when you expect an error and want to extract it for detailed assertions on the error fields.

Use `match` on the result when the error type has multiple variants and you need to verify specific variant details.

Don't fight the compiler here. Reach for `expect()` when in doubt. The message pays for itself in debugging time.

## Where to go next

- [What are error handling best practices](/en/what-are-error-handling-best-practices/)
- [Best Practices for Error Handling in Rust Libraries vs Applications](/en/best-practices-for-error-handling-in-rust-libraries-vs-applications/)
- [How to Propagate Errors from main() in Rust](/en/how-to-propagate-errors-from-main-in-rust/)