Variables and Mutability

Constants with const

A constant is a named value that is computed at compile time and never changes. It is declared with the const keyword, must have an explicit type, and can be declared in any scope, including outside every function.

const MAX_PLAYERS: u32 = 4;
const PI_APPROX: f64 = 3.14159;
const GREETING: &str = "Hello";
const SECONDS_PER_DAY: u32 = 60 * 60 * 24;
const PRIMES: [u32; 4] = [2, 3, 5, 7];

fn main() {
    println!("{MAX_PLAYERS} {PI_APPROX} {GREETING} {SECONDS_PER_DAY} {PRIMES:?}");
}
4 3.14159 Hello 86400 [2, 3, 5, 7]

Syntax

const NAME: Type = constant_expression;
Rule Detail
Keyword const replaces let
Type Always required. The compiler does not infer the type of a constant
Value Must be a constant expression, one the compiler can evaluate at compile time
Name SCREAMING_SNAKE_CASE by convention
Mutability Never mutable. mut is not allowed
Placement Module level (global) or inside any block, including function bodies

The Type Is Required

// ❌ does not compile
const LIMIT = 100;

fn main() {
    println!("{LIMIT}");
}
error: missing type for `const` item
 --> main.rs:2:12
  |
2 | const LIMIT = 100;
  |            ^ help: provide a type for the constant: `: i32`

Constants are part of an item's public interface and can be used from anywhere, so their types are always written out.

Constants Cannot Be mut

// ❌ does not compile
const mut LIMIT: u32 = 100;

fn main() {
    println!("{LIMIT}");
}
error: const globals cannot be mutable
 --> main.rs:2:7
  |
2 | const mut LIMIT: u32 = 100;
  |       ^^^ cannot be mutable
  |
help: you might want to declare a static instead
  |
2 - const mut LIMIT: u32 = 100;
2 + static mut LIMIT: u32 = 100;
  |

Assigning to a constant is also an error:

// ❌ does not compile
const LIMIT: u32 = 100;

fn main() {
    LIMIT = 200;
}
error[E0070]: invalid left-hand side of assignment
 --> main.rs:5:11
  |
5 |     LIMIT = 200;
  |     ----- ^
  |     |
  |     cannot assign to this expression

Naming

Constants use SCREAMING_SNAKE_CASE. Other casing compiles but produces a warning:

const lower: u32 = 1;

fn main() {
    println!("{lower}");
}
warning: constant `lower` should have an upper case name
 --> main.rs:1:7
  |
1 | const lower: u32 = 1;
  |       ^^^^^
  |
  = note: `#[warn(non_upper_case_globals)]` (part of `#[warn(nonstandard_style)]`) on by default
help: convert the identifier to upper case
  |
1 - const lower: u32 = 1;
1 + const LOWER: u32 = 1;
  |

The naming rule has a functional side. In a pattern, an uppercase name that resolves to a constant is treated as a constant to compare against, not as a new variable. A let that reuses a constant's name therefore does not create a new variable:

// ❌ does not compile
const LIMIT: u32 = 100;

fn main() {
    let LIMIT = 5;
    println!("{LIMIT}");
}
error[E0005]: refutable pattern in local binding
 --> main.rs:5:9
  |
2 | const LIMIT: u32 = 100;
  | ---------------- missing patterns are not covered because `LIMIT` is interpreted as a constant pattern, not a new variable
...
5 |     let LIMIT = 5;
  |         ^^^^^ patterns `0_u32..=99_u32` and `101_u32..=u32::MAX` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `u32`
help: introduce a variable instead
  |
5 |     let LIMIT_var = 5;
  |              ++++

Constant Expressions

The right-hand side of a const is evaluated by the compiler, so it can only use things the compiler is able to evaluate:

  • literals
  • arithmetic, bitwise, comparison, and logical operators
  • other constants
  • as casts between primitive types
  • tuples, arrays, struct and enum construction
  • calls to const fn functions, and to tuple struct and tuple variant constructors
  • if, match, loop, and while, inside a block
  • associated constants of types, such as u32::MAX
const fn square(n: u32) -> u32 {
    n * n
}

const AREA: u32 = square(12);
const BUFFER_SIZE: usize = 1 << 10;
const MASK: u8 = !0b0000_1111;
const IS_BIG: bool = AREA > 100;
const LETTER: char = 'x';
const MAX_AS_I64: i64 = u32::MAX as i64;

fn main() {
    let buffer = [0u8; BUFFER_SIZE];
    println!("{AREA} {} {MASK} {IS_BIG} {LETTER} {MAX_AS_I64}", buffer.len());
}
144 1024 240 true x 4294967295

BUFFER_SIZE is used as an array length, which must be known at compile time. Only a constant can fill that role; a let variable cannot.

A block can hold a multi-step computation, with local let mut variables and loops:

const SUM: u32 = {
    let mut total = 0;
    let mut i = 1;
    while i <= 10 {
        total += i;
        i += 1;
    }
    total
};

const LEN: usize = "hello".len();

fn main() {
    println!("{SUM} {LEN}");
}
55 5

"hello".len() works because str::len is a const fn. Compile-time evaluation, including const fn, is covered in its own section.

What Is Not Allowed

A call to an ordinary function is rejected, because the compiler cannot run it at compile time:

// ❌ does not compile
fn limit() -> u32 {
    100
}

const LIMIT: u32 = limit();

fn main() {
    println!("{LIMIT}");
}
error[E0015]: cannot call non-const function `limit` in constants
 --> main.rs:6:20
  |
6 | const LIMIT: u32 = limit();
  |                    ^^^^^^^
  |
note: function `limit` is not const
 --> main.rs:2:1
  |
2 | fn limit() -> u32 {
  | ^^^^^^^^^^^^^^^^^
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

Marking limit as const fn fixes it. Standard library functions that allocate or are not marked const are rejected the same way:

// ❌ does not compile
const NAME: String = String::from("rust");

fn main() {
    println!("{NAME}");
}
error[E0015]: cannot call non-const associated function `<String as From<&str>>::from` in constants
 --> main.rs:2:22
  |
2 | const NAME: String = String::from("rust");
  |                      ^^^^^^^^^^^^^^^^^^^^
  |
  = note: calls in constants are limited to constant functions, tuple structs and tuple variants

Runtime values cannot be used either, even in a const declared inside a function:

// ❌ does not compile
fn main() {
    let base = 10;
    const DOUBLE: i32 = base * 2;
    println!("{DOUBLE}");
}
error[E0435]: attempt to use a non-constant value in a constant
 --> main.rs:4:25
  |
4 |     const DOUBLE: i32 = base * 2;
  |                         ^^^^ non-constant value
  |
help: consider using `let` instead of `const`
  |
4 -     const DOUBLE: i32 = base * 2;
4 +     let DOUBLE: i32 = base * 2;
  |

A local const is an item. It does not capture local variables.

Arithmetic that overflows inside a constant is a compile-time error, not a runtime panic:

// ❌ does not compile
const BIG: u8 = 255 + 1;

fn main() {
    println!("{BIG}");
}
error[E0080]: attempt to compute `u8::MAX + 1_u8`, which would overflow
 --> main.rs:2:17
  |
2 | const BIG: u8 = 255 + 1;
  |                 ^^^^^^^ evaluation of `BIG` failed here

(The compiler also prints two erroneous constant encountered notes pointing at the uses of BIG.)

Some types are allowed in constants even though building them at runtime allocates. The empty constructors String::new() and Vec::new() are const fn, and no allocation happens until the first push:

const EMPTY_STRING: String = String::new();
const EMPTY_VEC: Vec<i32> = Vec::new();

fn main() {
    let mut s = EMPTY_STRING;
    s.push('a');
    let mut v = EMPTY_VEC;
    v.push(1);
    println!("{s} {v:?}");
}
a [1]

Each use of EMPTY_VEC produces a fresh empty vector.

Constants Are Inlined

A constant has no memory location of its own. The compiler substitutes the value at every place the name is used, as if the value had been written there. Every use is an independent copy.

const LIMIT: u32 = 100;

fn main() {
    let a = LIMIT + 1;       becomes      let a = 100 + 1;
    let b = LIMIT * 2;       becomes      let b = 100 * 2;
}

For small values this is free. For a large constant, such as a big array, each use can be a separate copy. When one fixed address is needed, or when a large value should be stored once, use a static item.

Interior Mutability Surprise

Because each use of a constant is a fresh copy, a constant whose type has interior mutability does not behave like shared state. This program increments COUNTER twice, and still reads 0:

use std::sync::atomic::{AtomicUsize, Ordering};

const COUNTER: AtomicUsize = AtomicUsize::new(0);
static REAL_COUNTER: AtomicUsize = AtomicUsize::new(0);

fn main() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
    COUNTER.fetch_add(1, Ordering::SeqCst);
    println!("const: {}", COUNTER.load(Ordering::SeqCst));
    REAL_COUNTER.fetch_add(1, Ordering::SeqCst);
    REAL_COUNTER.fetch_add(1, Ordering::SeqCst);
    println!("static: {}", REAL_COUNTER.load(Ordering::SeqCst));
}
const: 0
static: 2

The compiler warns about this pattern:

warning: mutation of an interior mutable `const` item with call to `fetch_add`
 --> main.rs:7:5
  |
7 |     COUNTER.fetch_add(1, Ordering::SeqCst);
  |     -------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |     |
  |     `COUNTER` is a interior mutable `const` item of type `Atomic<usize>`
  |
  = note: each usage of a `const` item creates a new temporary
  = note: only the temporaries and never the original `const COUNTER` will be modified

Shared state that must be observed from several places belongs in a static.

Scope and Placement

A constant can be declared at module level, where it is visible throughout the module (and to other modules with pub), or inside any block. Constants may refer to other constants regardless of their order of declaration at module level.

const FIRST: u32 = 10;
const SECOND: u32 = FIRST * 2;

fn main() {
    const LOCAL: u32 = SECOND + 1;
    println!("{FIRST} {SECOND} {LOCAL}");
    {
        const INNER: &str = "inner";
        println!("{INNER}");
    }
}
10 20 21

A local constant is visible only inside its block:

// ❌ does not compile
fn main() {
    {
        const INNER: u32 = 1;
    }
    println!("{INNER}");
}
error[E0425]: cannot find value `INNER` in this scope
 --> main.rs:6:16
  |
6 |     println!("{INNER}");
  |                ^^^^^ not found in this scope

Unlike a let variable, a const item can be used before the line that declares it, because items are visible throughout their whole scope.

String and Reference Constants

References in constants have the 'static lifetime. The lifetime can be omitted in a const declaration, and the compiler fills in 'static:

const GREETING: &'static str = "hi";
const OTHER: &str = "hello";

fn main() {
    println!("{GREETING} {OTHER}");
}
hi hello

Slices of fixed data are written as references to array literals:

const NAMES: &[&str] = &["ada", "grace", "linus"];
const ORIGIN: (i32, i32) = (0, 0);
const EMPTY: Option<u32> = None;

fn main() {
    for name in NAMES {
        println!("{name}");
    }
    println!("{ORIGIN:?} {EMPTY:?} {}", NAMES.len());
}
ada
grace
linus
(0, 0) None 3

Associated Constants

Types and traits can have constants too. An associated constant is declared with const inside an impl or trait block and is accessed with Type::NAME.

struct Circle {
    radius: f64,
}

impl Circle {
    const PI: f64 = 3.14159;
    const UNIT: Circle = Circle { radius: 1.0 };

    fn area(&self) -> f64 {
        Self::PI * self.radius * self.radius
    }
}

trait Shape {
    const SIDES: u32;
}

struct Triangle;
struct Square;

impl Shape for Triangle {
    const SIDES: u32 = 3;
}

impl Shape for Square {
    const SIDES: u32 = 4;
}

fn main() {
    println!("{} {}", Circle::PI, Circle::UNIT.area());
    println!("{} {}", Triangle::SIDES, <Square as Shape>::SIDES);
}
3.14159 3.14159
3 4

A trait can declare an associated constant without a value, and each implementing type supplies its own. The standard library's primitive types also have inherent associated constants, such as u32::MAX, i32::MIN, and f64::EPSILON.

Compile-Time Assertions

A constant with the name _ is evaluated but cannot be referred to. Combined with assert!, it checks a condition at compile time and stops compilation if the condition is false:

const _: () = assert!(std::mem::size_of::<u64>() == 8);

fn main() {
    println!("checked at compile time");
}
checked at compile time

If the condition is false, the build fails:

// ❌ does not compile
const _: () = assert!(std::mem::size_of::<u64>() == 4);

fn main() {}
error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<u64>() == 4
 --> main.rs:2:15
  |
2 | const _: () = assert!(std::mem::size_of::<u64>() == 4);
  |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `_` failed here

const Versus let

let const
Type annotation Optional Required
Value computed At runtime At compile time
Can be mut Yes No
Allowed outside functions No Yes
Can use runtime values Yes No
Can be an array length No Yes
Has a memory address Yes (a stack slot) No, the value is inlined at each use
Can be shadowed by a later let of the same name Yes No, a let with that name is treated as a pattern

Use const for fixed values that are known when the program is written: limits, sizes, mathematical constants, default settings, and lookup tables.