Constants

How Rust constants work, how they differ from immutable variables, and when to use them for compile-time known values.

Constants in Rust are values bound to a name that can never change — not just through reassignment, but even in principle. They are baked into the program at compile time and the compiler can freely replace every use of a constant with its literal value. This makes them distinct from immutable variables, which still involve run‑time storage and lifetime management.

Declaring a constant looks similar to a let binding but uses the const keyword and always requires an explicit type annotation.

const MAX_HEALTH: u32 = 100;
const APP_NAME: &str = "Guardian";

MAX_HEALTH and APP_NAME are now names for compile‑time values. Wherever you write MAX_HEALTH, the compiler substitutes 100u32 directly into the code — there is no memory allocation, no variable to read, and no way for the value to change later.

Type Annotation Is Mandatory:

Omitting the type on a const declaration is a compile‑time error. The compiler never infers the type for constants because that would open the door to subtle, hard‑to‑trace changes if a constant’s inferred type altered across module boundaries.

What Separates a Constant from an Immutable Variable

Rust already has immutable variables declared with let. A natural question is why the language needs a separate const keyword at all. The answer lies in when the value is known.

An immutable variable created with let receives its value at run time — even if the value looks like a literal, the binding itself exists as a piece of storage with a fixed address and a lifetime. You can read it, take references to it, and shadow it with another let binding. A constant, by contrast, exists only at compile time.

The four concrete differences that matter every day are:

  1. Constants are always immutable. There is no mut equivalent for const. You cannot change a constant after it is defined, and you cannot use mut in the declaration.
  2. Constants require a type annotation. The compiler must know the exact type at the definition site; there is no type inference for constants.
  3. Constants can appear in any scope — even global. You can declare a const at module level, inside a function, or inside a block. Immutable let bindings are local to the block they are declared in; there is no global let.
  4. Constants must be set to a constant expression. The right‑hand side must be computable by the compiler without running the program. That means no function calls unless the function is a const fn, no runtime values like user input, and no Vec::new().

These differences are not cosmetic. They arise from a fundamental distinction: constants are compile‑time entities, while all variables (even immutable ones) are run‑time entities.

Constants Don't Have a Fixed Address:

Because the compiler inlines a constant’s value at each use site, a constant does not occupy a stable place in memory. Taking a reference to a constant (e.g., &MAX_HEALTH) creates a temporary value that lives only as long as the reference itself. This can lead to surprising lifetime errors if you try to return a reference to a constant from a function.

Constant Expressions — What You Can Actually Put on the Right Side

The biggest practical constraint on constants is that the initializer must be a constant expression. A constant expression is any expression the compiler can evaluate entirely on its own, without running the program.

The following are all valid constant expressions:

  • Literal values: 5, true, 'A', "hello"
  • Arithmetic on literals or other constants: 60 * 60 * 24
  • Block expressions that contain only constant expressions
  • Calls to const fn (functions marked as evaluable at compile time)
const SECONDS_IN_DAY: u32 = 60 * 60 * 24;             // simple arithmetic
const GREETING: &str = {                               // block expression
    let _ = 42;
    "Hello, constants!"
};

Many standard library functions, like len() on arrays or pow() on integers, are not const fn. You cannot use them in a constant initializer.

const WRONG: usize = "hello".len(); // ERROR: `len()` is not a const fn
const ALSO_WRONG: u32 = 2.pow(3);   // ERROR: `pow()` is not const in Rust 2021

Non‑const Function Calls Are the Most Common Mistake:

When you first move a calculation into a constant, it is easy to reach for a familiar method like .len() or .pow() without checking whether it is const. The compiler will reject it with a message like “calls in constants are limited to constant functions.” The fix is either to compute the value manually with arithmetic, use a const fn alternative if one exists, or fall back to an immutable let binding computed at run time.

Work on constant evaluation is ongoing in Rust. New functions are regularly made const fn so they can be used at compile time. Check the documentation for the specific Rust edition you are targeting.

Where You Can Declare Constants

Constants are not restricted to function bodies. You can place them at module scope (outside any function), inside a function, or inside any block. This makes them ideal for shared values that many parts of a module need.

const WELCOME_MESSAGE: &str = "System ready.";
fn main() {
    println!("{}", WELCOME_MESSAGE);
    const LOCAL_HINT: u32 = 42;
    println!("{}", LOCAL_HINT);
}

Global constants behave like module‑level items: they are accessible to everything in the module (and to other modules if marked pub). A constant declared inside a function is scoped to that function — it exists only during compilation of that function, and its value is inlined wherever the function uses it.

Module‑level Constants Simplify Shared Configuration:

When a value like a buffer size, file path, or mathematical ratio is used across several functions, pulling it into a module‑level const with a descriptive name makes the code easier to read and removes the need to pass the value around. Because the compiler inlines it, there is no performance penalty.

Naming Conventions for Constants

Rust’s conventions (enforced by rustfmt and checked by Clippy) use SCREAMING_SNAKE_CASE for constants:

  • MAX_CONNECTIONS
  • DEFAULT_TIMEOUT_SECONDS
  • PI

This is a strong signal to anyone reading the code that the value is a compile‑time constant and will never change, in contrast to snake_case variable names. The convention is not enforced by the compiler itself, but failing to follow it will generate warnings from Clippy and confuse other Rust programmers.

Practical Use Cases

Constants are not just a stylistic choice. They solve real problems that appear in everyday code:

Eliminating magic numbers. A bare 86400 in a function body tells a reader nothing. const SECONDS_PER_DAY: u32 = 86400; tells the entire story at the point of use.

Compile‑time configuration. If a threshold, dimension, or flag is truly fixed for a given build, a constant lets the compiler optimize the entire program around that value. Dead branches that depend on a constant false can be eliminated entirely.

Avoiding accidental mutation. Because constants cannot be shadowed (you cannot write const FOO = … again in the same scope), there is no way to accidentally replace a constant later in the code. This is stronger than an immutable let, which can be shadowed by a new let binding with the same name.

Shared, simple data structures. Small arrays or tuples that are used read‑only throughout a module are natural fits for constants:

const WEEKDAYS: [&str; 5] = ["Mon", "Tue", "Wed", "Thu", "Fri"];

The array is compiled once, and references to it (like WEEKDAYS[2]) are replaced with the known value at compile time.

Common Pitfalls That Cause Compile Errors

Beyond the type‑annotation requirement and the constant‑expression constraint, a few other mistakes show up regularly:

Trying to shadow a constant with another const. You cannot declare a const with the same name in the same scope. This is different from let shadowing, which is legal. The compiler will tell you the name is already defined.

const VAL: i32 = 1;
const VAL: i32 = 2; // ERROR: name `VAL` is defined multiple times

Expecting a constant to have a memory address. As mentioned, &CONST creates a temporary. If you need a global value with a guaranteed fixed address (for example, to pass to C code via FFI), you need a static item, not a const.

Confusing compile‑time const with runtime immutability. A constant’s value is fixed for the entire program across all time. An immutable variable is fixed only for its lifetime; it can have different values in different function invocations or different iterations of a loop.

Constants and the Compiler: Why This Distinction Exists

Understanding the “why” helps when the constraints feel annoying. Rust keeps constants strictly compile‑time so that the compiler can treat them as values, not memory locations. This enables:

  • Inlining without penalty: Every use of a constant can be replaced by the literal value. No load instruction is ever needed.
  • Stronger dead‑code elimination: If a constant if DEBUG is false, the entire branch vanishes during compilation, not at runtime.
  • No initialization order problems: Global const items don’t need to be initialized in any particular order because they are not variables that hold state. They are just names for expressions the compiler already computed.

If the language blurred the line between constants and immutable variables, it would lose these guarantees. The separate syntax makes the programmer’s intent explicit and gives the compiler the information it needs.

Summary

Constants in Rust are a compile‑time mechanism for naming values that are truly fixed for the life of the program. They occupy no run‑time storage, can appear in any scope, and require an explicit type. Their right‑hand side must be calculable by the compiler alone.

The presence of both const and immutable let can feel redundant at first, but they serve different purposes: const is for values baked into the binary itself, while let is for run‑time bindings that happen to never change. When you need a stable memory address, a global variable that can be mutated inside an unsafe block, or a value initialized at run time once, you’ll reach for static rather than const.

If you walk away with one rule, make it this: if a value is truly known at compile time and will never need a memory address, use const; if the value is determined at run time or needs a lifetime, use let. The compiler will enforce the rest.