Testing Panic in Rust: How to Write Tests for Error Situations

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Panic is Not a Death Sentence: Why Test Error Situations in Rust?

\n\n

Imagine: you're writing a Rust library for working with money. The function divide_money(amount: u64, parts: u64) should panic if parts is zero — because you can't divide by zero, and you didn't want to return Option or Result for architectural reasons. How do you ensure this panic actually occurs? Or you're developing a parser that should abort when invalid data is passed. Without tests for panic, you risk "silent" bugs that only appear in production.

\n\n

In Rust, testing panic is not just a "nice option" but an important tool for creating reliable code. In this article, we'll explore how to write unit tests that verify your code actually panics in the right situations. You'll learn about the #[should_panic] macro, its nuances, alternatives like std::panic::catch_unwind, and best practices. Let's go!

\n\n

The Classic Approach: The #[should_panic] Macro

\n\n

The simplest and most common way to check for a panic in Rust is to use the #[should_panic] attribute. It is placed above a test function and tells the compiler: "This test is considered successful only if a panic occurs inside."

\n\n

Basic Example

\n\n
// lib.rs or tests/unit_tests.rs\nfn divide(a: i32, b: i32) -> i32 {\n    if b == 0 {\n        panic!("Division by zero!");\n    }\n    a / b\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    #[should_panic]\n    fn test_divide_by_zero() {\n        divide(10, 0);\n    }\n}
\n\n

Run the tests with the command cargo test — and you'll see a green result. If no panic occurs (for example, if you remove the zero check), the test will fail with the message: "test test_divide_by_zero ... FAILED (expected to panic)".

\n\n

Checking the Exact Panic Message

\n\n

Sometimes you need to verify not just the fact of a panic, but its specific message. This helps avoid false positives when a panic occurs due to a different error. Use the additional expected parameter:

\n\n
#[test]\n#[should_panic(expected = "Division by zero!")]\nfn test_divide_by_zero_with_message() {\n    divide(10, 0);\n}\n\n// This test will fail — the message is different\n#[test]\n#[should_panic(expected = "Division by zero!")]\nfn test_other_panic() {\n    panic!("A completely different error");\n}
\n\n

Important: expected checks that the panic message contains the specified string (not that it is an exact match). This is convenient for long messages.

\n\n

Advanced Approach: std::panic::catch_unwind

\n\n

Sometimes #[should_panic] is not enough. For example, if you want to verify that a panic occurred in a specific place and not somewhere else, or if you need to test asynchronous code. For such cases, there is std::panic::catch_unwind — a function that catches a panic and returns it as a Result.

\n\n

How It Works

\n\n
use std::panic;\n\nfn risky_function(should_panic: bool) {\n    if should_panic {\n        panic!("Emergency!");\n    }\n}\n\n#[test]\nfn test_catch_panic() {\n    let result = panic::catch_unwind(|| {\n        risky_function(true);\n    });\n    assert!(result.is_err()); // Check that a panic occurred\n}\n\n#[test]\nfn test_no_panic() {\n    let result = panic::catch_unwind(|| {\n        risky_function(false);\n    });\n    assert!(result.is_ok()); // No panic occurred\n}
\n\n

Checking the Panic Type

\n\n

With catch_unwind, you can even check that the panic contains a specific value (not just a string):

\n\n
use std::panic;\nuse std::any::Any;\n\nfn panic_with_number() {\n    panic!(42); // Yes, this is possible — a panic with a number!\n}\n\n#[test]\nfn test_panic_with_number() {\n    let resu                

Blogs

Book Recommendations