Question
I have a Rust function that panics under certain conditions, and I want to write a unit test to verify that the panic actually happens. I found assert! and assert_eq!, but I did not see an obvious way to test for panics.
Is there a built-in mechanism for this in Rust, or should I spawn a separate task and check whether that task panics?
Returning a Result<T, E> is not suitable in my case.
For example, I want to implement the Add trait for a Matrix type. I want matrix addition to support syntax like this:
let m = m1 + m2 + m3;
where m1, m2, and m3 are matrices.
That means the result type of add should be Matrix. A style like this would be too awkward:
let m = ((m1 + m2).unwrap() + m3).unwrap();
However, matrix addition must validate that both matrices have the same dimensions. If the dimensions do not match, add() should panic. In that case, how can I write a Rust unit test that confirms the panic occurs?
Short Answer
By the end of this page, you will understand how Rust tests can check for panics, when to use #[should_panic], when std::panic::catch_unwind is helpful, and how this applies to APIs like Add implementations that panic on invalid input.
Concept
In Rust, a panic signals that the program has reached an unrecoverable state. In tests, you sometimes want to verify that invalid input correctly triggers that panic.
Rust provides a built-in way to do this with the #[should_panic] test attribute. If a test marked with #[should_panic] panics, the test passes. If it does not panic, the test fails.
This matters because some APIs are intentionally designed to panic when misused. Common examples include:
- indexing past the end of a collection
- calling methods that require a valid invariant
- operator overloads such as
Addwhen invalid operands should be considered a programmer error
In your matrix example, implementing Add for Matrix usually means the method must return Matrix, not Result<Matrix, E>, because the trait signature is fixed around an output type. If mismatched dimensions are considered invalid usage, panicking can be a reasonable design choice.
When that is your design, your tests should explicitly confirm both behaviors:
- valid matrices add correctly
- invalid matrices panic
Rust testing supports this directly, so you do not need to spawn a new task just to detect a panic in most cases.
Mental Model
Think of a panic like a built-in emergency stop.
- Normal return value: the function finished its work normally.
Result: the function expected something might go wrong and lets the caller handle it.- Panic: the function detected a broken rule and stops immediately.
A panic test is like checking that a machine's emergency shutdown triggers when safety conditions are violated.
For matrix addition:
- same dimensions -> normal operation
- different dimensions -> emergency stop
Your test is not asking, "Did the math work?" It is asking, "Did the safety rule activate when it should?"
Syntax and Examples
The simplest way to test for a panic in Rust is #[should_panic].
Basic syntax
#[test]
#[should_panic]
fn test_that_panics() {
panic!("something went wrong");
}
This test passes because the function panics.
Example with matrix validation
use std::ops::Add;
#[derive(Debug, PartialEq, Clone)]
struct Matrix {
rows: usize,
cols: usize,
data: Vec<i32>,
}
impl Add for Matrix {
type Output = Matrix;
fn add(self, rhs: Matrix) -> Matrix {
if self.rows != rhs.rows || self.cols != rhs.cols {
panic!("matrix dimensions must match");
}
let data = self
.data
.()
.(rhs.data.())
.(|(a, b)| a + b)
.();
Matrix {
rows: .rows,
cols: .cols,
data,
}
}
}
() {
= Matrix {
rows: ,
cols: ,
data: [, , , ],
};
= Matrix {
rows: ,
cols: ,
data: [, , , , , ],
};
= m1 + m2;
}
Step by Step Execution
Consider this test:
#[test]
#[should_panic(expected = "matrix dimensions must match")]
fn add_panics_with_clear_message() {
let m1 = Matrix {
rows: 1,
cols: 2,
data: vec![1, 2],
};
let m2 = Matrix {
rows: 2,
cols: 1,
data: vec![3, 4],
};
let _ = m1 + m2;
}
Here is what happens step by step:
- The test runner starts
add_panics_with_clear_message. - Because of
#[should_panic(...)], Rust expects this test to panic. m1andm2are created.- The expression
m1 + m2calls theaddmethod from theAddtrait implementation. - Inside
add, the code compares and .
Real World Use Cases
Testing panics is useful in real Rust code when your API treats invalid usage as a programmer error.
Common scenarios
- Operator overloading:
Add,Sub, or indexing operations may panic when invariants are violated. - Input validation for internal APIs: helper functions may panic if they are only meant to be called with already-validated data.
- Collection access: code that intentionally uses indexing may rely on panic behavior for invalid indexes.
- Debug-only invariant checks: libraries may panic when internal assumptions are broken.
- Constructor validation: some constructors panic if impossible states are requested.
Matrix example
A matrix library might choose:
checked_add(&self, &other) -> Result<Matrix, MatrixError>for recoverable user-facing codeimpl Add for Matrixthat panics on invalid dimensions for ergonomic operator syntax
This gives users two styles:
- safe, explicit error handling
- concise operator syntax when inputs are trusted
Real Codebase Usage
In real projects, developers often combine panic-based APIs with safer alternatives.
Common patterns
1. Ergonomic operator + checked method
impl Matrix {
fn checked_add(self, rhs: Matrix) -> Result<Matrix, &'static str> {
if self.rows != rhs.rows || self.cols != rhs.cols {
return Err("matrix dimensions must match");
}
let data = self
.data
.into_iter()
.zip(rhs.data.into_iter())
.map(|(a, b)| a + b)
.collect();
Ok(Matrix {
rows: self.rows,
cols: self.cols,
data,
})
}
}
Then Add can delegate to it and panic if needed:
impl Add {
= Matrix;
(, rhs: Matrix) Matrix {
.(rhs)
.()
}
}
Common Mistakes
1. Using #[should_panic] without narrowing the cause
This test passes on any panic:
#[test]
#[should_panic]
fn test_matrix_add() {
let _ = buggy_code();
}
Problem:
- the panic might come from unrelated code
Better:
#[test]
#[should_panic(expected = "matrix dimensions must match")]
fn test_matrix_add() {
let _ = buggy_code();
}
2. Forgetting to trigger the panic
Broken example:
#[test]
#[should_panic]
fn test_add_should_panic() {
let m1 = make_matrix_2x2();
let m2 = make_matrix_3x2();
// forgot to actually add them
}
Comparisons
| Approach | When to use | Pros | Cons |
|---|---|---|---|
#[should_panic] | A whole test should panic | Simple and built-in | Less flexible |
#[should_panic(expected = "...")] | You want to verify the reason | Catches wrong panic causes better | Still tied to panic text |
std::panic::catch_unwind | You need programmatic panic detection inside a test | More control | More verbose |
Returning Result<T, E> | Failure is expected and recoverable | Explicit and flexible | Not always compatible with trait ergonomics |
#[should_panic] vs
Cheat Sheet
#[test]
#[should_panic]
fn test_name() {
some_code_that_should_panic();
}
#[test]
#[should_panic(expected = "message text")]
fn test_name() {
some_code_that_should_panic();
}
use std::panic;
#[test]
fn test_with_catch_unwind() {
let result = panic::catch_unwind(|| {
some_code_that_should_panic();
});
assert!(result.is_err());
}
Rules
#[should_panic]makes the test pass only if a panic occurs.expected = "..."checks that the panic message contains the given text.- You usually do not need a separate thread to test panics.
- Use
Resultfor recoverable errors. - Use panic for broken assumptions or invariant violations.
Good practice
FAQ
How do I assert that a function panics in Rust?
Use the #[should_panic] attribute on a test function.
Can I check the panic message too?
Yes. Use #[should_panic(expected = "some text")] to match part of the message.
Do I need to spawn a thread or task to test a panic?
No. In normal unit tests, #[should_panic] is the standard solution.
When should I use catch_unwind instead of #[should_panic]?
Use catch_unwind when you need to inspect panic behavior inside a larger test or keep executing more assertions afterward.
Is panicking in Add a bad idea?
Not necessarily. If mismatched matrix dimensions represent invalid usage and you want ergonomic + syntax, panicking can be reasonable.
Should matrix addition return Result instead?
That depends on your API design. Result is better for recoverable errors, but trait-based operator syntax often favors returning the concrete output type.
Can one Rust test verify both panic and non-panic behavior?
Yes. catch_unwind is useful when one test needs to check both cases programmatically.
Mini Project
Description
Build a small Matrix type in Rust that supports addition with the + operator and panics when the dimensions do not match. This project demonstrates both normal unit testing and panic testing using Rust's built-in test tools.
Goal
Create a Matrix type with Add support, write one test for successful addition, and one test that confirms invalid dimensions cause a panic.
Requirements
- Define a
Matrixstruct with row count, column count, and data storage. - Implement the
Addtrait forMatrix. - Panic if the dimensions of the two matrices do not match.
- Write at least one passing addition test.
- Write at least one panic test using
#[should_panic].
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Associated Types vs Generic Type Parameters in Rust: When to Use Each
Learn when to use associated types vs generic parameters in Rust traits, with clear rules, examples, and practical API design advice.
Can a Struct Extend Another Struct in Rust? Composition vs Inheritance
Learn how Rust handles struct reuse without inheritance, using composition, traits, and wrapper structs with practical examples.