Constants

A constant in Rust is a name bound to a value that is fixed for the entire run of the program and known at compile time. Constants are declared with the const keyword, and while they look similar to immutable variables declared with let, they are actually a different kind of language item entirely: the compiler computes their value ahead of time and substitutes it directly at every place the name is used, rather than storing it in a memory slot that gets read at runtime. Understanding constants well means understanding this distinction, because it explains both what constants are useful for and why the compiler is so strict about what you can put inside one.

Overview: What Is a Constant?

When you write let x = 5;, you create a variable binding. Even without mut, that binding still technically lives somewhere at runtime (typically the stack), and its value could in principle come from a computation that only finishes while the program is running — for example let x = read_input();. A const is different: its value must be computable entirely by the compiler, before the program ever runs. You cannot initialize a constant with the result of a function call to your own runtime logic, a value read from a file, or anything that depends on program state. This is not a style preference — it is enforced by the compiler.

Because a constant’s value is known at compile time, the compiler is free to treat its name as a stand-in for the literal value itself. Wherever MAX_POINTS appears in your code, the compiler can substitute 100_000 directly, the same way a macro expands to its replacement text. This means constants generally do not occupy a single fixed memory address the way a global variable would — they are inlined wherever they’re used. Rust does offer a separate item, static, for values that genuinely need one fixed address for the life of the program (useful for FFI or certain low-level patterns), but for ordinary named constants you almost always want const.

Constants can be declared in any scope: at the top level of a module (so they’re visible throughout a file, or exported if marked pub), or inside a function body where they’re only visible locally. Unlike variables, a constant is never bound to a specific memory location that could be borrowed and mutated, so ownership and borrowing rules that govern let bindings mostly don’t apply to constants themselves — you’re always working with a copy of the value each time you reference one.

const vs let vs static

Feature let const static
Mutable? Only with mut Never, under any circumstances Only via unsafe (rare, discouraged)
Value known at Runtime (may depend on computation) Compile time only Compile time only
Type annotation Optional (usually inferred) Required Required
Memory behavior Lives in a normal variable location Inlined at each use site One fixed address for the program’s lifetime
Scope Block-local only Any scope, including module-level and global Any scope, including module-level and global

Syntax

The general form of a constant declaration is:

const NAME: TYPE = VALUE;
  • const — the keyword that introduces the item. Note there is no const mut; constants can never be mutable.
  • NAME — by strong convention, written in SCREAMING_SNAKE_CASE. The compiler will emit a lint warning (not an error) if you use lowercase.
  • TYPE — mandatory. Unlike let, Rust never infers the type of a constant; you must spell it out (for example u32, f64, &str, or [i32; 4]).
  • VALUE — must be a constant expression: something the compiler can fully evaluate at compile time, such as a literal, simple arithmetic on literals, or a call to a function explicitly marked const fn.

Examples

Example 1: A Basic Constant

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("The maximum number of points is: {}", MAX_POINTS);
}

Output:

The maximum number of points is: 100000

Here MAX_POINTS is declared outside of main, at module scope, which is a common place for constants since it makes them visible to every function in the file. The underscore inside 100_000 is purely a visual separator for readability — it has no effect on the value, which is why the output prints 100000 without the underscore.

Example 2: Using Constants in Calculations

const SECONDS_PER_MINUTE: u32 = 60;
const MINUTES_PER_HOUR: u32 = 60;

fn seconds_in_hours(hours: u32) -> u32 {
    hours * MINUTES_PER_HOUR * SECONDS_PER_MINUTE
}

fn main() {
    let hours = 3;
    let total_seconds = seconds_in_hours(hours);
    println!("{} hours is {} seconds", hours, total_seconds);
}

Output:

3 hours is 10800 seconds

This example shows constants being combined with a runtime value (hours, an ordinary let binding) inside a function. SECONDS_PER_MINUTE and MINUTES_PER_HOUR themselves never change and are computed once by the compiler, but they can freely take part in arithmetic alongside values that are only known while the program is running. Giving these numbers names instead of writing bare 60s throughout the function makes the intent of the calculation obvious.

Example 3: Constants for Array Sizes and Local Scope

const MAX_ITEMS: usize = 5;

fn main() {
    let inventory: [&str; MAX_ITEMS] = ["sword", "shield", "potion", "map", "torch"];

    for item in inventory.iter() {
        println!("Item: {}", item);
    }

    const GREETING: &str = "Hello, adventurer!";
    println!("{}", GREETING);
}

Output:

Item: sword
Item: shield
Item: potion
Item: map
Item: torch
Hello, adventurer!

Two things are worth noticing here. First, MAX_ITEMS is used as the length of a fixed-size array, [&str; MAX_ITEMS] — array lengths must be known at compile time, so only a const (or a literal) works there; a let binding would not compile. Second, GREETING is declared right inside main, showing that constants aren’t restricted to module scope — a function-local constant is visible only within that function, just like a local variable would be.

How It Works Step by Step

Consider this small program:

const AREA: u32 = 10 * 20;

fn main() {
    println!("Area: {}", AREA);
}

Output:

Area: 200

Here is what the compiler actually does with it:

  1. While type-checking the crate, the compiler encounters const AREA: u32 = 10 * 20; and evaluates 10 * 20 immediately, during compilation — not when the program runs. It checks that the result, 200, fits in a u32.
  2. The compiler records that every occurrence of the name AREA refers to the compile-time-known value 200.
  3. When it reaches the println! call inside main, it substitutes AREA with 200 as part of code generation — there is no runtime lookup, no memory read from a global variable, and no risk of the value changing between now and when the program executes.
  4. The compiled binary simply prints the literal 200; the multiplication 10 * 20 never happens at runtime at all, because it was already folded away during compilation.

This is also why constants are so restricted in what their initializer can contain: the compiler must be able to fully carry out step 1 without running your program. A call to a regular function, a value read from the environment, or anything involving I/O simply cannot be evaluated at that stage.

Common Mistakes

Mistake 1: Forgetting the Type Annotation

Unlike let, Rust never infers a constant’s type from its value. Omitting the type is a compile error, not a warning:

const MAX_POINTS = 100_000;

fn main() {
    println!("{}", MAX_POINTS);
}

The compiler rejects this with something like:

error: missing type for `const` item
help: provide a type for the item: `MAX_POINTS: i32`

The fix is to always write out the type explicitly:

const MAX_POINTS: u32 = 100_000;

fn main() {
    println!("Max points: {}", MAX_POINTS);
}

Output:

Max points: 100000

Mistake 2: Trying to Reassign a Constant

New Rust learners sometimes expect a const to behave like a variable that merely starts out immutable. It doesn’t — there is no way to make a constant mutable, ever, not even with mut:

const MAX: i32 = 5;

fn main() {
    MAX = 10;
    println!("{}", MAX);
}

This fails to compile because MAX is not a memory location that can be written to — it’s a compile-time value inlined wherever it appears:

error[E0070]: invalid left-hand side of assignment
note: cannot assign to this expression because it is a `const` item, not a variable

If you need a value that can change, use a regular variable with let mut instead of const:

fn main() {
    let mut max = 5;
    max = 10;
    println!("{}", max);
}

Output:

10

Best Practices

  • Use SCREAMING_SNAKE_CASE for constant names — it’s the idiomatic convention, and the compiler will lint against lowercase names.
  • Prefer named constants over "magic numbers" scattered through your code; const MAX_RETRIES: u32 = 3; is far clearer than a bare 3 reused in five places.
  • Reach for const whenever a value needs to be known at compile time, such as array lengths, buffer sizes, or mathematical constants.
  • Only use static when you specifically need a fixed memory address (for example, interfacing with C code); for ordinary fixed values, const is almost always the right choice.
  • Keep constant expressions simple and self-contained. If a value genuinely requires runtime computation or external input, it belongs in a regular variable or a function, not a constant.
  • Group related constants in a dedicated module (for example mod config { ... }) when several parts of a crate need to share them.
  • Always give constants their most precise type — prefer u8 over i32 for a value that truly never exceeds 255, since this documents intent and can catch mistakes elsewhere.

Practice Exercises

  1. Declare a constant named SPEED_OF_LIGHT of type f64 equal to 299_792_458.0 (meters per second), and print it with a descriptive message inside main.
  2. Declare a constant BOARD_SIZE: usize equal to 8, use it as the length of an array of char values representing one row of a chessboard, and print each element with a loop.
  3. Without running the compiler, predict what error message you would get if you wrote const LIMIT: i32 = get_limit(); where get_limit is an ordinary (non-const) function that returns i32. Then explain in your own words why the compiler rejects it.

Summary

  • A const binds a name to a value that must be fully computable by the compiler, before the program runs.
  • Constants always require an explicit type annotation — Rust never infers it.
  • Constants are never mutable, under any circumstances; there is no const mut.
  • Constant values are typically inlined at every use site rather than stored at one memory address, unlike static items.
  • Constants can be declared at module scope (visible throughout, or exported with pub) or inside a function body (local to that function).
  • Use constants for compile-time-known limits such as array sizes, and prefer them over scattering magic numbers through your code.