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 noconst mut; constants can never be mutable.NAME— by strong convention, written inSCREAMING_SNAKE_CASE. The compiler will emit a lint warning (not an error) if you use lowercase.TYPE— mandatory. Unlikelet, Rust never infers the type of a constant; you must spell it out (for exampleu32,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 markedconst 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:
- While type-checking the crate, the compiler encounters
const AREA: u32 = 10 * 20;and evaluates10 * 20immediately, during compilation — not when the program runs. It checks that the result,200, fits in au32. - The compiler records that every occurrence of the name
AREArefers to the compile-time-known value200. - When it reaches the
println!call insidemain, it substitutesAREAwith200as 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. - The compiled binary simply prints the literal
200; the multiplication10 * 20never 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_CASEfor 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 bare3reused in five places. - Reach for
constwhenever a value needs to be known at compile time, such as array lengths, buffer sizes, or mathematical constants. - Only use
staticwhen you specifically need a fixed memory address (for example, interfacing with C code); for ordinary fixed values,constis 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
u8overi32for a value that truly never exceeds 255, since this documents intent and can catch mistakes elsewhere.
Practice Exercises
- Declare a constant named
SPEED_OF_LIGHTof typef64equal to299_792_458.0(meters per second), and print it with a descriptive message insidemain. - Declare a constant
BOARD_SIZE: usizeequal to8, use it as the length of an array ofcharvalues representing one row of a chessboard, and print each element with a loop. - Without running the compiler, predict what error message you would get if you wrote
const LIMIT: i32 = get_limit();whereget_limitis an ordinary (non-const) function that returnsi32. Then explain in your own words why the compiler rejects it.
Summary
- A
constbinds 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
staticitems. - 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.
