---
title: How to Write Compile-Time Assertions in Rust
summary: Use assert! or const_assert! macros to enforce conditions at compile time in Rust.
lang: en
slug: how-to-write-compile-time-assertions-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-write-compile-time-assertions-in-rust/
---

# How to Write Compile-Time Assertions 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.

```rust
/// 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.

```rust
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.

```rust
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.

```rust
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`.

```rust
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.

```rust
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

- [How to Use Const Generics for Array-Length Generics](/en/how-to-use-const-generics-for-array-length-generics/)
- [How to Use the TypeId for Runtime Type Identification](/en/how-to-use-the-typeid-for-runtime-type-identification/)
- [What are blanket implementations](/en/what-are-blanket-implementations/)