How to Write Compile-Time Assertions in Rust

Use assert! or const_assert! macros to enforce conditions at compile time in Rust.

The buffer size must be a power of two

You define a buffer size as a constant. The rest of your code assumes this size is a power of two because it uses bitwise shifts for alignment. If someone changes the constant to 15, the math breaks silently. The bug hides until a user hits a weird edge case weeks later. You need the compiler to reject the build the moment that constant stops being a power of two.

Assertions that run before the binary exists

A compile-time assertion is a check that runs while the compiler is translating your code into machine instructions. If the check fails, the compiler stops and prints an error. Your program never gets built.

Think of it like a safety inspector on a construction site. The inspector walks through the blueprints before a single brick is laid. If the load-bearing wall is missing, the inspector halts construction immediately. You don't find out the wall is missing when the building collapses under the first snowstorm. You find out before you pour the concrete.

Compile-time assertions catch logical errors, layout assumptions, and invariant violations at the earliest possible moment. The check disappears after the build. You pay zero runtime cost for compile-time safety.

Minimal example: const fn with panic

The most flexible way to write a compile-time assertion is a const fn that panics when the condition is false.

/// Checks that a value is a power of two, failing compilation if not.
const fn assert_power_of_two(value: u32) {
    // A panic inside a const fn stops compilation immediately.
    // The compiler catches the panic and turns it into a hard error.
    if value == 0 || (value & (value - 1)) != 0 {
        panic!("Buffer size must be a power of two, got {}", value);
    }
}

const BUFFER_SIZE: u32 = 16;

// This call happens during compilation.
// If the condition fails, the compiler emits an error here.
assert_power_of_two(BUFFER_SIZE);

What the compiler does with your assertion

When the compiler sees assert_power_of_two(BUFFER_SIZE), it doesn't generate a function call for runtime. It evaluates the function right then and there. The function body runs in the compiler's internal const evaluator.

If the if condition is true, the panic! macro triggers. The compiler catches this panic and converts it into a hard error. The build stops. The error message includes your panic text, pointing to the location of the assertion.

If the condition is false, the function returns normally, and the compiler moves on. No code for this check ends up in your binary. The check is completely erased after compilation succeeds. This is why compile-time assertions are free. They cost nothing in the final executable.

Realistic example: enforcing struct layout

Hardware interfaces and FFI boundaries often require strict memory layouts. You can assert these properties using std::mem::size_of and std::mem::align_of, which are const-stable.

use std::mem::{size_of, align_of};

/// Ensures the Packet struct fits exactly in 64 bytes for hardware alignment.
const fn assert_packet_layout() {
    let size = size_of::<Packet>();
    let align = align_of::<Packet>();

    if size != 64 {
        panic!("Packet must be exactly 64 bytes, but is {}", size);
    }
    if align != 8 {
        panic!("Packet must be 8-byte aligned, but is {}", align);
    }
}

struct Packet {
    header: u32,
    payload: [u8; 60],
}

// Verify the layout assumption at compile time.
// If the struct changes, the build fails immediately.
assert_packet_layout();

Treat struct layout assumptions like firewalls. Verify them at the boundary, not when the packet drops.

Enforcing generic constraints with const items

When you use const generics, you can enforce constraints on the generic parameter by defining a const item inside the implementation. This runs for every instantiation of the generic type.

struct Buffer<const N: usize>;

impl<const N: usize> Buffer<N> {
    // This const item runs for every instantiation of Buffer<N>.
    // If N is 0, the build fails with the panic message.
    // The type () is a convention for assertions that don't produce a value.
    const _: () = assert!(N > 0, "Buffer size must be greater than zero");
}

// This compiles fine.
let _valid = Buffer::<10>;

// This fails to compile with the panic message.
// let _invalid = Buffer::<0>;

The pattern const _: () = assert!(...) is idiomatic for generic constraints. The underscore name signals that the item exists solely for its side effect (the assertion). The type () indicates the assertion doesn't produce a usable value.

Newer Rust versions provide const_assert! in the standard library, which offers cleaner syntax for the same result.

impl<const N: usize> Buffer<N> {
    // const_assert! is equivalent to the const item pattern but more readable.
    const_assert!(N > 0, "Buffer size must be greater than zero");
}

Use const_assert! when you prefer macro syntax over the const item boilerplate. It produces the same compile-time check with less visual noise.

Pitfalls and compiler errors

Compile-time assertions are powerful, but they have limits. The const evaluator is a sandbox. It cannot do everything a runtime function can do.

Calling non-const functions

You cannot call regular functions inside a const context. If you try to use println! or a helper that isn't marked const, the compiler rejects you with E0015 (functions in constants are restricted). You must ensure every function in the call chain is const.

const fn bad_assertion() {
    // This fails with E0015.
    // println! is not const.
    println!("Checking value");
}

No allocation

Const functions cannot allocate memory. You cannot create a Vec or call String::from inside a const assertion. If your check requires dynamic structures, you must refactor to use arrays or primitive types. The compiler enforces this strictly.

The panic message matters

When a const assertion fails, the compiler emits E0080 (evaluation panicked). The error message includes your panic text. If you write a vague message, debugging becomes painful.

const fn vague_assertion(value: u32) {
    if value == 0 {
        // Bad: "assertion failed" tells you nothing.
        panic!("assertion failed");
    }
}

const fn clear_assertion(value: u32) {
    if value == 0 {
        // Good: Tells you exactly what to fix.
        panic!("Value must be non-zero, got 0. Check the configuration constant.");
    }
}

If the error message is confusing, the assertion is useless. Write panic messages that tell you exactly what to fix.

Convention: static_assertions crate

The Rust community has a strong convention around readability. While writing a custom const fn works, many projects prefer the static_assertions crate. It provides macros like assert_eq_size! and assert_type_eq_all! that produce cleaner error messages and require less boilerplate.

Using the crate signals to other Rustaceans that you care about maintainable assertions. It also saves you from reinventing the wheel for common checks like alignment or size. The crate is widely used in the ecosystem. If you see static_assertions in a Cargo.toml, you know the author values compile-time guarantees.

Decision matrix

Choose the right tool based on your needs. Each option has a specific place in the toolbox.

Use a custom const fn with panic! when you need a detailed error message or complex logic that standard macros don't cover.

Use the assert! macro in a const context when the check is a simple boolean expression and the default compiler message is sufficient.

Use const_assert! from the standard library when you prefer a macro syntax over a function call for simple conditions.

Use the static_assertions crate when you want readable, self-documenting macros for common checks like size, alignment, or type equality.

Use the const _: () = assert!(...) pattern when you need to enforce constraints on generic parameters inside an impl block and are on an older Rust version without const_assert!.

Reach for runtime assertions when the value depends on input data that isn't known until the program runs.

Compile-time checks are free insurance. Buy them for every assumption your code makes.

Where to go next