Variables and Mutability
14 min read
Understanding variables in Rust is crucial. Unlike JavaScript/TypeScript where everything is mutable by default, Rust makes variables immutable by default. This is the single biggest mindset shift you’ll need to make.
Quick Overview
Section titled “Quick Overview”In Rust, variables are immutable unless explicitly marked as mutable with mut. This forces you to think about which data needs to change and which doesn’t, leading to safer and more predictable code.
Key takeaway: let creates immutable bindings, let mut creates mutable bindings.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”// TypeScript/JavaScript - everything mutable by defaultlet x = 5;x = 6; // OK - can reassign
const y = 10;// y = 11; // Error - const prevents reassignment
// But const doesn't prevent mutation!const arr = [1, 2, 3];arr.push(4); // OK - array itself is mutable!arr[0] = 99; // OK// arr = []; // Error - can't reassign
const obj = { value: 42 };obj.value = 43; // OK - object is mutable!// obj = {}; // Error - can't reassignKey points:
let= mutable (can reassign)const= can’t reassign, but contents can mutate- Mutability is the default
Rust Equivalent
Section titled “Rust Equivalent”// Rust - immutable by default!let x = 5;// x = 6; // Compile error: cannot assign twice to immutable variable
let mut y = 10;y = 11; // OK - explicitly marked as mutable
// Arrays/vectorslet arr = vec![1, 2, 3];// arr.push(4); // Error - arr is immutable// arr[0] = 99; // Error
let mut arr2 = vec![1, 2, 3];arr2.push(4); // OKarr2[0] = 99; // OK
// Constants (compile-time constants)const MAX_POINTS: u32 = 100_000;// MAX_POINTS = 200_000; // Error - constants are never mutableKey points:
let= immutable (cannot reassign or mutate)let mut= mutable (can reassign and mutate)const= compile-time constant (must have type annotation)- Immutability is the default
Detailed Explanation
Section titled “Detailed Explanation”Immutable Variables
Section titled “Immutable Variables”fn main() { let x = 5; println!("The value of x is: {}", x);
// x = 6; // This would cause a compile error: // error[E0384]: cannot assign twice to immutable variable `x`}Why immutable by default?
- Safety: Prevents accidental modification
- Concurrency: Immutable data is safe to share across threads
- Optimization: Compiler can make better optimizations
- Intent: Makes your intentions explicit
This is the opposite of JavaScript!
Mutable Variables
Section titled “Mutable Variables”fn main() { let mut x = 5; println!("The value of x is: {}", x);
x = 6; // OK - x is mutable println!("The value of x is: {}", x);}Output:
The value of x is: 5The value of x is: 6When to use mut:
- Counters in loops
- Accumulators
- Any value that needs to change over time
Shadowing
Section titled “Shadowing”Rust has a unique feature called shadowing that lets you redeclare a variable:
fn main() { let x = 5;
let x = x + 1; // OK - shadowing
{ let x = x * 2; // OK - shadows in this scope println!("Inner scope: {}", x); // 12 }
println!("Outer scope: {}", x); // 6}Output:
Inner scope: 12Outer scope: 6Shadowing vs Mutation:
// Shadowing - creates new variablelet x = 5;let x = x + 1; // New variable, can change type
// Mutation - changes existing variablelet mut y = 5;y = y + 1; // Same variable, same typeShadowing allows type changes:
let spaces = " "; // &strlet spaces = spaces.len(); // OK - now usize
let mut spaces2 = " ";// spaces2 = spaces2.len(); // Error - type mismatchConstants
Section titled “Constants”// Constants must:// 1. Have a type annotation// 2. Be assigned a constant expression (evaluated at compile time)// 3. Use SCREAMING_SNAKE_CASE naming
const MAX_POINTS: u32 = 100_000;const PI: f64 = 3.14159;const APP_NAME: &str = "MyApp";
fn main() { println!("Max points: {}", MAX_POINTS); // MAX_POINTS = 200_000; // Error - constants are always immutable}Note: “Constant expression” does not mean “no function calls.” A
constcan call anyconst fnand many standard-library methods that are markedconst, as long as everything is evaluable at compile time. For example,const NAME_LEN: usize = "MyApp".len();and a call to your ownconst fn double(n: u32) -> u32 { n * 2 }both work. What you cannot do is call a non-constfunction (anything that needs to run at runtime, like reading the clock or allocating aVec).
Constants vs Immutable Variables:
| Feature | const | let |
|---|---|---|
| Mutability | Never | Unless mut |
| Type annotation | Required | Optional (inferred) |
| Computation | Compile-time only | Runtime OK |
| Scope | Global or local | Local only |
| Naming convention | SCREAMING_SNAKE_CASE | snake_case |
| Can be shadowed | No | Yes |
When to use const:
- Configuration values
- Mathematical constants
- String literals used multiple times
- Values that truly never change
Key Differences from TypeScript/JavaScript
Section titled “Key Differences from TypeScript/JavaScript”1. Default Mutability
Section titled “1. Default Mutability”JavaScript:
let x = 5; // Can changeconst y = 5; // Can't reassign (but contents can mutate)Rust:
let x = 5; // Can't changelet mut y = 5; // Can changeconst Z: i32 = 5; // Can't change, compile-time constantMental model shift: Rust’s let is stricter than JavaScript’s const.
JavaScript const only blocks reassignment of the binding — the value it
points to can still be mutated (arr.push(4)). Rust’s immutable let blocks
both reassignment and mutation of the value. (And don’t confuse this with
Rust’s own const, which is a separate, compile-time-only construct covered
above — not the everyday tool for local bindings.)
2. Mutation vs Reassignment
Section titled “2. Mutation vs Reassignment”JavaScript const:
const arr = [1, 2, 3];arr.push(4); // OK - array is mutablearr[0] = 99; // OKarr = []; // Error - can't reassignRust let:
let arr = vec![1, 2, 3];// arr.push(4); // Error - vector is immutable// arr[0] = 99; // Error// arr = vec![]; // ErrorIn Rust, immutable means truly immutable!
3. Shadowing
Section titled “3. Shadowing”JavaScript:
let x = 5;let x = 10; // SyntaxError: Identifier 'x' has already been declaredRust:
let x = 5;let x = 10; // OK - shadowing4. Type Changes
Section titled “4. Type Changes”TypeScript:
let spaces = " ";spaces = spaces.length; // Type error: string to numberRust with shadowing:
let spaces = " ";let spaces = spaces.len(); // OK - shadowing allows type changeRust with mutation:
let mut spaces = " ";// spaces = spaces.len(); // Error - can't change typeCommon Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Forgetting mut
Section titled “Pitfall 1: Forgetting mut”Problem:
fn main() { let counter = 0;
for i in 1..=5 { counter += 1; // Error: cannot assign to immutable variable }}Error (the compiler also warns that counter and i are unused, since the code never compiles far enough to use them):
warning: variable `counter` is assigned to, but never used --> src/main.rs:2:9 |2 | let counter = 0; | ^^^^^^^ | = note: consider using `_counter` instead = note: `#[warn(unused_variables)]` on by default
warning: unused variable: `i` --> src/main.rs:4:9 |4 | for i in 1..=5 { | ^ help: if this is intentional, prefix it with an underscore: `_i`
error[E0384]: cannot assign twice to immutable variable `counter` --> src/main.rs:5:9 |2 | let counter = 0; | ------- first assignment to `counter`...5 | counter += 1; // Error: cannot assign to immutable variable | ^^^^^^^^^^^^ cannot assign twice to immutable variable |help: consider making this binding mutable |2 | let mut counter = 0; | +++Solution:
fn main() { let mut counter = 0; // Add 'mut'
for i in 1..=5 { counter += 1; // OK }
println!("Counter: {}", counter);}Pitfall 2: Thinking const Works Like TypeScript
Section titled “Pitfall 2: Thinking const Works Like TypeScript”Problem:
const MAX_POINTS = 100_000; // Error: missing type annotationError:
error: missing type for `const` item --> src/main.rs:1:17 |1 | const MAX_POINTS = 100_000; // Error: missing type annotation | ^ help: provide a type for the constant: `: i32`Solution:
const MAX_POINTS: u32 = 100_000; // OK - type annotation requiredPitfall 3: Trying to Mutate Immutable Data
Section titled “Pitfall 3: Trying to Mutate Immutable Data”Problem:
fn main() { let v = vec![1, 2, 3]; v.push(4); // Error: cannot borrow as mutable}Error:
error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable --> src/main.rs:3:5 |3 | v.push(4); // Error: cannot borrow as mutable | ^ cannot borrow as mutable |help: consider changing this to be mutable |2 | let mut v = vec![1, 2, 3]; | +++Solution:
fn main() { let mut v = vec![1, 2, 3]; // Add 'mut' v.push(4); // OK}Pitfall 4: Confusing Shadowing with Mutation
Section titled “Pitfall 4: Confusing Shadowing with Mutation”Problem thinking:
let x = 5;let x = 6; // "This is reassignment, right?"Reality: It’s shadowing - a new variable with the same name.
fn main() { let x = 5; println!("Address: {:p}", &x);
let x = 6; println!("Address: {:p}", &x); // a different address}The two &x print different addresses, which proves these are two separate
variables rather than one variable being reassigned. The actual addresses and
the gap between them are not specified by the language — they depend on the
platform, the optimization level, and stack layout, so don’t read anything into
the exact hex values or assume a fixed offset.
These are different variables in memory!
Best Practices
Section titled “Best Practices”1. Prefer Immutability
Section titled “1. Prefer Immutability” Don’t default to mut:
fn main() { let mut x = 5; // Unnecessary mut let mut y = 10; // Unnecessary mut println!("{} {}", x, y);} Only use mut when needed:
fn main() { let x = 5; // Immutable - won't change let mut y = 10; // Mutable - will change y += 5; println!("{} {}", x, y);}Why: Immutability makes code easier to reason about and enables compiler optimizations.
2. Use Shadowing for Transformations
Section titled “2. Use Shadowing for Transformations”Don’t create new variable names:
let spaces_str = " ";let spaces_count = spaces_str.len();let spaces_display = format!("Count: {}", spaces_count);Use shadowing for the same concept:
let spaces = " ";let spaces = spaces.len();let spaces = format!("Count: {}", spaces);When shadowing makes sense:
- Type transformations
- Progressive calculations
- Parsing/validation steps
3. Use Constants for True Constants
Section titled “3. Use Constants for True Constants”Don’t use variables for constants:
fn main() { let max_points = 100_000; // Used everywhere // ... lots of code ...} Use const for values that never change:
const MAX_POINTS: u32 = 100_000;
fn main() { // MAX_POINTS available everywhere}4. Clear Variable Names
Section titled “4. Clear Variable Names”Don’t use generic names:
let mut x = vec![];let mut y = vec![];Use descriptive names:
let mut users = vec![];let mut products = vec![];Even with mutability, clarity matters!
Real-World Example
Section titled “Real-World Example”Calculating Running Average
Section titled “Calculating Running Average”TypeScript:
function calculateRunningAverage(numbers: number[]): number[] { let sum = 0; const averages = [];
for (let i = 0; i < numbers.length; i++) { sum += numbers[i]; averages.push(sum / (i + 1)); }
return averages;}
const nums = [10, 20, 30, 40, 50];console.log(calculateRunningAverage(nums));// [10, 15, 20, 25, 30]Rust:
fn calculate_running_average(numbers: &[i32]) -> Vec<f64> { let mut sum = 0; let mut averages = Vec::new();
for (i, &num) in numbers.iter().enumerate() { sum += num; averages.push(sum as f64 / (i + 1) as f64); }
averages}
fn main() { let nums = vec![10, 20, 30, 40, 50]; let result = calculate_running_average(&nums); println!("{:?}", result); // [10.0, 15.0, 20.0, 25.0, 30.0]}Notice:
sumandaveragesaremut(they change)numbersis immutable (just reading)- Clear intent about what changes
Configuration with Constants
Section titled “Configuration with Constants”TypeScript:
const CONFIG = { MAX_CONNECTIONS: 100, TIMEOUT_MS: 5000, API_URL: "https://api.example.com",};
function connect() { // Use CONFIG.MAX_CONNECTIONS}Rust:
const MAX_CONNECTIONS: u32 = 100;const TIMEOUT_MS: u64 = 5000;const API_URL: &str = "https://api.example.com";
fn connect() { // Use MAX_CONNECTIONS}
fn main() { println!("Max connections: {}", MAX_CONNECTIONS);}Constants are available globally without runtime overhead!
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”- The Rust Book - Variables and Mutability
- Rust Reference - Constants
- Rust by Example - Variable Bindings
Related Topics
Section titled “Related Topics”Exercises
Section titled “Exercises”Exercise 1: Fix the Mutability
Section titled “Exercise 1: Fix the Mutability”Fix this code:
fn main() { let x = 5; println!("x is: {}", x);
x = 6; println!("x is now: {}", x);}Solution
fn main() { let mut x = 5; // Add 'mut' println!("x is: {}", x);
x = 6; println!("x is now: {}", x);}Exercise 2: Use Shadowing
Section titled “Exercise 2: Use Shadowing”Rewrite using shadowing to convert a string to its length:
fn main() { let text = "Hello, Rust!"; let text_length = text.len(); println!("Text: '{}' has length {}", text, text_length);}Solution
fn main() { let text = "Hello, Rust!"; println!("Text: '{}'", text);
let text = text.len(); // Shadow with new type println!("Length: {}", text);}Exercise 3: Constants
Section titled “Exercise 3: Constants”Create constants for a game:
- Maximum health: 100
- Starting gold: 50
- Player name: “Hero”
// Add constants here
fn main() { println!("Max health: {}", /* use constant */); println!("Starting gold: {}", /* use constant */); println!("Player: {}", /* use constant */);}Solution
const MAX_HEALTH: u32 = 100;const STARTING_GOLD: u32 = 50;const PLAYER_NAME: &str = "Hero";
fn main() { println!("Max health: {}", MAX_HEALTH); println!("Starting gold: {}", STARTING_GOLD); println!("Player: {}", PLAYER_NAME);}Exercise 4: Counter
Section titled “Exercise 4: Counter”Write a function that counts from 1 to n:
fn count_to(n: i32) { // Implement using a mutable counter}
fn main() { count_to(5); // Should print the numbers 1 to 5, space-separated}Solution
fn count_to(n: i32) { let mut counter = 1; while counter <= n { print!("{} ", counter); counter += 1; } println!();}
fn main() { count_to(5);}Summary
Section titled “Summary”What you’ve learned:
- Rust variables are immutable by default
- Use
let mutfor mutable variables - Shadowing lets you reuse names and change types
- Constants require type annotations and are compile-time
- Immutability leads to safer, more maintainable code
Key syntax:
let x = 5; // Immutablelet mut y = 10; // Mutableconst Z: i32 = 100; // Constant
let x = x + 1; // Shadowing (new variable)y = y + 1; // Mutation (same variable)Mental model:
- Default to immutable
- Add
mutonly when needed - Use shadowing for transformations
- Use
constfor true constants
This is the foundation! Everything else in Rust builds on this concept.