assert!, debug_assert! and unreachable!

An assertion states a fact the code relies on and stops the program when the fact is false, which is cheaper than letting the wrong value travel through the rest of the program. Rust has three forms: assert! for a condition, assert_eq! and assert_ne! for a comparison, and debug_assert! for a check that is too expensive for a release build.

This lesson covers the forms, the build flag that turns the debug ones off, the macro for a case that cannot happen and the attribute that warns when a result is dropped. The last section shows the message a failed assertion prints, which is the first thing to read in a test log.

assert and assert_eq

assert! takes a boolean and an optional message; assert_eq! takes two values and prints both when they differ. The comparison needs PartialEq and the message needs Debug.

The program checks a sum with all three forms and prints the value, so the pass is visible in the output. The checks pass, and the print is the only output.

fn main() { let total = 2 + 2; assert!(total > 0); assert_eq!(total, 4); assert_ne!(total, 5); println!("{total}"); }

rustc --edition 2024 assert.rs
./assert

4

The checks run in every build, including the release one, which is the difference from the next section and the reason a test can rely on them. The lesson on tests uses the same macros.

debug_assert

debug_assert! runs only when debug assertions are on, which is the default without -O. A release build removes the check, so it must never carry a side effect the program needs.

The program checks a value that is too large and prints before and after. The debug build panics at the check, the release build prints both lines.

fn main() { let value = 10; println!("before"); debug_assert!(value < 5, "value must be small"); println!("after {value}"); }

rustc --edition 2024 debug_assert.rs
./debug_assert
rustc -O --edition 2024 debug_assert.rs -o debug_release
./debug_release

before

thread 'main' (1800) panicked at debug_assert.rs:4:5:
value must be small
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The two runs differ only in the check, which is the point of the flag. The lesson on build variants lists the other differences.

unreachable!

unreachable! marks a branch the programmer believes cannot happen. It panics like an assertion, with the message given, and it also tells the compiler that the branch never returns.

The program classifies one and two but the function only handles zero and one. The second call reaches the macro and panics with the message.

fn classify(n: i32) -> &'static str { match n { 0 => "zero", 1 => "one", _ => unreachable!("only zero and one are handled"), } } fn main() { println!("{}", classify(1)); println!("{}", classify(2)); }

rustc --edition 2024 unreachable.rs
./unreachable

one

thread 'main' (1814) panicked at unreachable.rs:5:14:
internal error: entered unreachable code: only zero and one are handled
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The panic text starts with internal error, which marks the case as a bug in the program rather than a bad input. A match with a missing arm is a compile error instead, which is the better tool when the set is closed.

must_use

#[must_use] on a function warns when the return value is dropped. It is the attribute behind the warnings on iterators and Results, and it catches a call whose result the program forgot to read.

The program calls the function and drops the result, and the call still runs. The warning names the call and suggests let _ = for the deliberate case.

#[must_use] fn compute(value: i32) -> i32 { value * 2 } fn main() { compute(3); println!("done"); }

rustc --edition 2024 must_use.rs
./must_use

warning: unused return value of `compute` that must be used
--> must_use.rs:7:5
|
7 | compute(3);
| ^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
|
7 | let _ = compute(3);
| +++++++

warning: 1 warning emitted

done

The program still prints done, because the warning is not an error. A build with --deny warnings from the lesson on warnings would stop it.

A failed assertion

A failed assert_eq! prints the two values and the custom message, then panics with status 101. The two sides are labelled left and right, which is the order of the arguments.

The program asserts that two plus three is four, with the message sum mismatch. The output is the comparison, the two numbers and the message.

fn main() { let expected = 4; let actual = 2 + 3; assert_eq!(actual, expected, "sum mismatch"); println!("{actual}"); }

rustc --edition 2024 assert_fail.rs
./assert_fail
echo "status: $?"


thread 'main' (1816) panicked at assert_fail.rs:4:5:
assertion `left == right` failed: sum mismatch
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The line and column point at the macro call, not at the values. A test report from the next lesson shows the same block under the test name.

Exercises

  1. Add an assertion with a message to a function that computes an average.
  2. Compile the debug-assert program with and without -O and compare the two outputs.
  3. Put #[must_use] on a function and call it without using the result.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. When does assert! run?

    Answer

    In every build

    assert is not tied to a profile, unlike debug_assert.

  2. When does debug_assert! run?

    Answer

    When debug assertions are on, which is the default without -O

    A release build removes the check, so it must not carry a side effect.

  3. What does a failed assert_eq! print?

    Answer

    The two values and the message

    The macro shows both sides so a reader can see what differed.

  4. What does #[must_use] do?

    Answer

    Warns when the return value is dropped

    The attribute marks a result the caller should read, and the warning catches the forget.

  5. What does this program print? fn main() { let total = 2 + 2; assert!(total > 0); assert_eq!(total, 4); println!("{total}"); }

    Answer

    4

    Both assertions hold, and the value prints.

  6. What does this program print? fn main() { let value = 3; debug_assert!(value < 5, "small"); println!("after {value}"); }

    Answer

    after 3

    The check passes because three is below five, and the print shows the value.

  7. What does this program print? fn check(n: i32) -> &'static str { match n { 0 => "zero", _ => unreachable!("only zero"), } } fn main() { println!("{}", check(0)); }

    Answer

    zero

    Zero takes its own arm; the unreachable arm is only for a value the caller should not pass.

  8. What does this program print? #[must_use] fn compute(value: i32) -> i32 { value * 2 } fn main() { compute(3); println!("done"); }

    Answer

    done

    The dropped result is a warning, not an error, and the print still runs.

  9. What is the first line of the panic message for a failed assert_eq!?

    Answer

    assertion left == right failed

    The macro reports the comparison that failed before the two values.

  10. Why must a debug_assert! not change program state?

    Answer

    A release build removes the whole expression

    The code inside disappears with -O, so any side effect would be missing there.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev