How to Use assert!, assert_eq!, and assert_ne! in Rust

Use `assert!` to verify a condition is true, `assert_eq!` to check two values are equal, and `assert_ne!` to ensure they are different; all three panic if the check fails. Place these macros inside test functions annotated with `#[test]` or directly in `main` to validate logic immediately.

The moment your assumption breaks

You write a function to calculate a discount. You test it with a price of 100 and a discount of 10. It returns 90. You ship it. Three days later, a user passes a discount of 200. The price goes negative. The inventory system crashes because negative prices break the accounting logic. You didn't have a guard. You assumed the input was valid. Rust gives you tools to stop that assumption from becoming a runtime disaster. assert! is that guard. It screams when your code's expectations are violated.

Assertions are contracts, not control flow

Rust's ownership system prevents memory bugs. Assertions prevent logic bugs. An assertion is a declaration that a condition must be true at a specific point in the program. If the condition is false, the program panics. A panic is an unrecoverable error. It unwinds the stack, prints a message, and terminates the process.

Think of assertions as a smoke detector for your logic. You do not install a smoke detector to control the lights. You install it because if it goes off, something is fundamentally wrong. In Rust, assert! panics the program if the condition is false. assert_eq! checks equality. assert_ne! checks inequality. They are not if statements. They are declarations of truth. If the truth fails, the program stops.

Assertions belong in code where a failure indicates a bug in the program, not a problem with the user's input. If a user provides invalid data, that is an error. You handle errors with Result or Option. If your internal state becomes impossible, that is a bug. You catch bugs with assertions.

Minimal example

The assert! macro takes a boolean expression. If the expression is true, execution continues. If it is false, the macro calls panic!.

fn calculate_tax(amount: f64, rate: f64) -> f64 {
    // Validate the rate is within bounds.
    // If this fails, the program panics immediately.
    assert!(rate >= 0.0 && rate <= 1.0, "Tax rate must be between 0.0 and 1.0");

    amount * rate
}

fn main() {
    let tax = calculate_tax(100.0, 0.08);
    println!("Tax: {}", tax);
}

The assert_eq! macro compares two values. It checks if they are equal using the == operator. If they are not equal, it panics and prints both values. This makes debugging much easier than a raw assert!.

fn add(left: i32, right: i32) -> i32 {
    left + right
}

fn main() {
    let result = add(2, 2);
    // Verify the result matches the expected value.
    // The panic message will show both values if this fails.
    assert_eq!(result, 4);
}

The assert_ne! macro ensures two values are different. It panics if they are equal.

fn main() {
    let status = "active";
    // Ensure the status has changed from the default.
    assert_ne!(status, "inactive");
}

Convention aside: The community standard for assert_eq! is assert_eq!(expected, actual). The macro prints left == right in the panic message. Putting the expected value first makes the failure message read like a specification. assert_eq!(4, result) produces assertion failed: 4 == result. This highlights the expectation immediately.

What happens under the hood

Macros expand into code before compilation. assert! expands to an if statement that checks the condition. If the condition is false, it invokes panic! with a formatted message. The message includes the source file, line number, and the expression that failed.

assert_eq! expands to check left == right. If the comparison is false, it formats the values using the Debug trait and panics. This expansion reveals two requirements. First, the values must implement PartialEq to support ==. Second, the values must implement Debug to support printing in the error message.

If you try to assert on a type that does not implement Debug, the compiler rejects the code. You will see E0277 (trait bound not satisfied). The error message tells you that Debug is required for the assertion. This is a safety feature. It forces you to make your types inspectable before you can assert on them.

struct SecretKey {
    data: [u8; 32],
}

fn main() {
    let key1 = SecretKey { data: [0; 32] };
    let key2 = SecretKey { data: [0; 32] };
    // This fails to compile.
    // SecretKey does not implement Debug.
    // assert_eq!(key1, key2);
}

To fix this, derive Debug on the struct.

#[derive(Debug, PartialEq)]
struct SecretKey {
    data: [u8; 32],
}

The PartialEq derive is also needed for ==. Without it, you get E0308 or a trait bound error depending on the context. Assertions require both comparison and display capabilities.

Realistic example: Protecting invariants

Assertions shine when protecting invariants. An invariant is a condition that must always be true for a data structure to be valid. If the invariant breaks, the data structure is corrupted. Assertions catch corruption at the moment it happens.

Consider a BankAccount. The balance must never be negative. The withdraw method must enforce this.

#[derive(Debug, PartialEq)]
struct BankAccount {
    balance: u64,
}

impl BankAccount {
    /// Creates a new account with an initial balance.
    fn new(balance: u64) -> Self {
        BankAccount { balance }
    }

    /// Withdraws funds, enforcing the non-negative invariant.
    fn withdraw(&mut self, amount: u64) {
        // Check if the withdrawal exceeds the balance.
        // This is a logic error if called incorrectly.
        assert!(
            self.balance >= amount,
            "Insufficient funds: balance={}, amount={}",
            self.balance,
            amount
        );

        self.balance -= amount;
    }
}

fn main() {
    let mut account = BankAccount::new(100);
    account.withdraw(50);
    assert_eq!(account.balance, 50);
}

The assert! here uses a custom message with format arguments. This is useful when the default message is not enough. The message includes the relevant state. When the assertion fails, the panic output shows exactly why.

Custom messages are optional. The default message is often sufficient. Use custom messages when the context is not obvious from the expression alone.

Pitfalls and compiler errors

Assertions are powerful, but they have traps. The most common trap is using assert! for control flow. If you use assert! to validate user input, your program panics on bad input. A panic crashes the process. In a web server, this drops the connection and kills the worker. In a CLI tool, this prints a stack trace and exits. Neither is a good user experience.

User input is fallible. Fallible operations return Result or Option. Do not assert on user input. Assert on internal state. If a function receives a u32 and assumes it is non-zero, assert that it is non-zero. If a function receives a string from the user and needs to parse it, return an error if parsing fails.

Another trap is the difference between assert! and debug_assert!. The assert! macro runs in both debug and release builds. The debug_assert! macro only runs in debug builds. In release builds, debug_assert! is stripped out by the compiler.

If you put a critical safety check in debug_assert!, it disappears in production. Bugs slip through. If you put an expensive check in assert!, it runs in production and slows down the code. Use assert! for checks that must hold in release. Use debug_assert! for checks that are expensive or only useful for debugging.

Convention aside: The community treats assert! as "this must never happen, ever" and debug_assert! as "this is a sanity check, might be slow, skip in production". If you are unsure, use assert!. Performance can be optimized later. Safety cannot.

Compiler errors appear when types do not match. assert_eq!(1, "one") fails with E0308 (mismatched types). The compiler expects both sides to be the same type. You cannot compare an integer to a string. You must convert one side or use a different comparison.

fn main() {
    let count = 5;
    let label = "5";
    // This fails with E0308.
    // assert_eq!(count, label);
}

Fix this by converting the types.

fn main() {
    let count = 5;
    let label = "5";
    assert_eq!(count.to_string(), label);
}

When to use what

Use assert! when you are enforcing a safety invariant that must hold in production, such as array bounds, non-null pointers in safe wrappers, or state machine transitions.

Use assert_eq! when you need to compare two values and require the panic message to display both the left and right operands for debugging.

Use assert_ne! when you need to ensure two values are distinct, such as verifying a state transition actually changed the value or detecting a duplicate entry.

Use debug_assert! when the check is computationally expensive or only useful during development, and you want the compiler to remove it from release builds.

Use Result or Option when the condition represents a recoverable error, such as invalid user input, a missing configuration, a network failure, or a file not found.

Use panic! directly when you have a fatal error that cannot be recovered from and you want to provide a custom message without a condition check.

Treat assertions as the boundary between "this is a bug" and "this is an error". If the failure means your code is wrong, assert. If the failure means the world is messy, return a result.

Where to go next