How to Use Compile-Time Computation (const fn) in Rust

Define a function with the `const fn` keyword to allow the compiler to evaluate it at compile time for constant expressions.

When runtime is too late

You are building a network protocol. The packet header size depends on a few flags. You calculate the size in main, but then you realize you need that size to allocate a fixed-size buffer. Or worse, you are writing a library and you want to guarantee that a user's configuration is valid before the program even starts. Runtime checks are slow and error-prone. You want the compiler to do the math and reject bad inputs before the binary exists. That is where const fn comes in.

Move the work to the compiler. It is free CPU cycles that never touch your users.

The compiler as a calculator

Rust's ownership model keeps memory safe, but it also gives you tools to push work from runtime to compile time. A const fn is a function that the compiler can execute while building your program. Normal functions run when your program runs. const fn runs when you run cargo build. The result gets baked into the binary.

Think of it like a recipe that the chef follows while reading the menu, not while serving the customer. If the recipe fails, the menu is rejected. If the recipe works, the dish is pre-made and ready to serve instantly.

The compiler has a restricted subset of Rust for const fn. It is a sandbox. You can do arithmetic, loops, recursion, and struct construction. You cannot allocate memory, call trait methods, or use println. The compiler checks the function body against this list. If you try something forbidden, compilation fails. If the function panics during evaluation, compilation also fails. This makes const fn a powerful tool for validation. You can write a constructor that panics on invalid input, and any attempt to create an invalid value at compile time will break the build.

The compiler is your worker. Give it math, not magic.

Minimal example

Here is a basic const fn. It calculates the square of a number. The compiler evaluates it when you use it in a const context.

/// Calculates the square of a number at compile time.
const fn square(x: i32) -> i32 {
    // Basic arithmetic is allowed in const fn.
    x * x
}

// The compiler calls square(6) now, not when the program runs.
// The result is baked into the binary as the literal 36.
const ANSWER: i32 = square(6);

fn main() {
    // ANSWER is just 36. No function call exists in the runtime code.
    println!("Answer: {}", ANSWER);
}

Notice the const keyword before fn. This marks the function as eligible for compile-time evaluation. It does not force evaluation. The compiler only evaluates the function when it has all the arguments at compile time. If you call square with a runtime variable, it runs at runtime. The const keyword is a capability, not a command.

Convention aside: You will see const and static used for global values. They look similar but behave differently. const values are copied everywhere they are used. static values live at one fixed address. Use const for simple data like numbers and strings. Use static when you need a pointer to the value, like for thread-local storage or FFI.

How evaluation works

When the compiler encounters a const fn, it runs a stricter check than for normal functions. It verifies that every operation inside is allowed in a constant context. If you try to allocate memory or call a trait method, the compiler rejects you with E0015 (calls in statics may only call const functions) or a similar error about unsupported operations.

Once the function passes the check, the compiler evaluates it with the provided arguments. The result replaces the call site. In the example above, const ANSWER: i32 = square(6); becomes const ANSWER: i32 = 36; in the generated code. There is no function call overhead. There is no stack frame. The value is just there.

This process happens recursively. If square called another const fn, the compiler would evaluate that one too. You can build complex compile-time computations by composing small const fn blocks.

The compiler has a limit on how much work it will do. If you write a const fn that loops too many times or recurses too deeply, you will hit a "overflow evaluating const requirement" error. This protects you from accidentally making compilation take forever. Keep const functions simple. If you need heavy computation, consider generating the data externally or using a build script.

Real-world pattern: compile-time validation

The most common use of const fn in production code is validation. You can enforce invariants at compile time, ensuring invalid inputs break the build rather than crashing at runtime.

/// A configuration struct that validates its fields at compile time.
struct Config {
    max_connections: u32,
}

impl Config {
    /// Creates a new Config, panicking if max_connections is zero.
    /// This panic happens at compile time if used in a const context.
    const fn new(max_connections: u32) -> Config {
        // Panicking in const fn is allowed and is the standard way to validate.
        if max_connections == 0 {
            panic!("max_connections must be greater than zero");
        }
        Config { max_connections }
    }
}

// This compiles fine. The compiler checks the value and accepts it.
const GOOD_CONFIG: Config = Config::new(10);

// This fails to compile. The panic is caught by the compiler.
// Uncommenting this line will break the build with a clear error message.
// const BAD_CONFIG: Config = Config::new(0);

fn main() {
    // You can also call const fn at runtime.
    // The validation still happens, but at runtime.
    let runtime_config = Config::new(5);
    println!("Max connections: {}", runtime_config.max_connections);
}

This pattern is powerful. Library authors use it to guarantee that users cannot create invalid states. If a user passes a bad value, they get a compiler error pointing to the exact line. No runtime crash. No obscure bug. The feedback loop is immediate.

Convention aside: const fn constructors are a community standard for validation. If a value can be invalid, make the constructor const fn so users get feedback immediately. This is especially important for types that wrap external resources or enforce complex invariants.

Const blocks: inline evaluation

Rust 1.79 introduced const blocks, which allow you to evaluate expressions at compile time inside functions. This is useful when you need a compile-time value but don't want to define a separate function.

fn main() {
    // const blocks evaluate at compile time even inside functions.
    // This is useful for inline calculations or validation.
    let result = const { 2 + 2 };
    println!("Result: {}", result);

    // You can also use const blocks for validation.
    let config = const {
        let max = 10;
        if max == 0 {
            panic!("max cannot be zero");
        }
        max
    };
    println!("Config: {}", config);
}

Const blocks are evaluated at compile time if the compiler can determine all the values. If the block depends on runtime data, it falls back to runtime evaluation. This gives you flexibility without sacrificing compile-time guarantees when possible.

Use const blocks for small, inline computations. If the logic is complex or reused, define a const fn instead. This keeps your code organized and reusable.

Pitfalls and limits

const fn is a powerful tool, but it has limitations. Understanding these will save you from frustration.

Calling a const fn at runtime: If you call a const fn with runtime arguments, it runs at runtime. The const keyword does not force compile-time evaluation. The compiler only evaluates it when it has all the arguments at compile time. This is by design. It allows const fn to be used in both contexts.

Recursion depth: The compiler limits how deep const fn recursion can go. If you write a factorial function that goes too deep, you will hit a "overflow evaluating const requirement" error. Keep const functions simple or iterative. If you need deep recursion, consider a different approach.

Traits: You generally cannot use trait bounds in const fn. If you need polymorphism, const fn might not be the right tool. The compiler will reject trait methods with errors about unsupported operations. This is a known limitation. The Rust team is working on improving trait support in const contexts, but for now, stick to concrete types.

Compile time cost: Compile time is a resource. Just like runtime CPU and memory, compile time has a limit. If you write a const fn that computes a massive lookup table, your build times will suffer. Other developers cloning your repo will wait longer for cargo build to finish. Use const fn for logic that saves runtime work or enforces safety, not just because you can. Balance runtime performance against developer experience.

Remember: const fn is a capability, not a command. The compiler evaluates it only when it can.

Decision matrix

Use const fn when you need to compute values during compilation, like lookup tables or configuration validation. Use const fn when you want to enforce invariants at compile time, ensuring invalid inputs break the build rather than crashing at runtime. Use const fn when you are writing a library and want to allow users to initialize complex types in const contexts. Reach for static when you need a value with a fixed memory address, such as for thread-local storage or FFI bindings. Reach for lazy_static or std::sync::OnceLock when you need runtime initialization that happens once, like reading a file or connecting to a database. Reach for normal functions when the computation depends on runtime data or requires dynamic allocations that const fn does not support.

Pick the tool that matches your timing needs. Compile time for constants, runtime for dynamics.

Where to go next