ZetCode

Rust Mutability

last modified October 25, 2026

In this article we explore mutability in Rust: how to change values, how references limit changes, and how to change data through shared access.

In Rust, variables are immutable by default. A value that is bound to a name cannot be changed unless we say so with the mut keyword. This helps to prevent bugs, because we can see from the code which values may change. The compiler enforces the rule, so accidental modifications are compile-time errors, not run-time surprises.

Mutability is also the base of the borrowing rules. A value can be read through many shared references (&T), or changed through exactly one mutable reference (&mut T), but never both at the same time. The rules prevent data races and invalid references already at compile time.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q. The error messages are the real output of the compiler. For the basics of variables, see the variable tutorial; borrowing is explained in detail in the ownership and borrowing tutorial.

Immutable variables

A binding created with let is immutable. We can read the value, use it in expressions, and move it into other bindings, but we cannot assign a new value to it.

main.rs
fn main() {
    let x = 5;
    println!("The value of x is {x}");

    let y = x + 1;
    println!("The value of y is {y}");

    let name = String::from("Alice");
    let greeting = format!("Hello, {name}!");
    println!("{greeting}");
}

We create three immutable bindings and read them.

let x = 5;
let y = x + 1;

The variable x is never changed. New values are created from it; y is a separate binding with its own value.

let name = String::from("Alice");
let greeting = format!("Hello, {name}!");

Immutability applies to all types, not only to numbers. The name string cannot be modified; format! builds a new string.

$ cargo run -q
The value of x is 5
The value of y is 6
Hello, Alice!

Assigning to an immutable variable

Assigning a second value to an immutable variable is an error.

main.rs
fn main() {
    let x = 5;
    println!("x = {x}");

    x = 6;
    println!("x = {x}");
}

The program does not compile.

let x = 5;
x = 6;

The compiler reports the error E0384 and points to the first assignment. It also suggests the fix: write let mut x. Initializing a variable later, with let x; and one assignment, is allowed; the error appears only for the second assignment.

$ cargo run -q
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:5:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
...
5 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0384`.

The mut keyword

To make a binding mutable, we put mut after let.

main.rs
fn main() {
    let mut counter = 0;
    println!("Counter: {counter}");

    counter += 1;
    println!("Counter: {counter}");

    counter *= 3;
    println!("Counter: {counter}");

    let mut text = String::from("Rust");
    text.push_str(" is fun");
    println!("{text}");
}

We change an integer and a string.

let mut counter = 0;
counter += 1;
counter *= 3;

The compound assignment operators change the variable in place. The type of the variable cannot change; counter stays an integer.

let mut text = String::from("Rust");
text.push_str(" is fun");

Methods that modify a value, such as push_str, require a mutable binding. A good habit is to start with let and add mut only when the compiler asks for it.

$ cargo run -q
Counter: 0
Counter: 1
Counter: 3
Rust is fun

Mutability belongs to the binding

The mut keyword is a property of the binding, not of the value. When we move a value to a new binding, the new binding decides whether the value can be changed.

main.rs
fn main() {
    let frozen = vec![1, 2, 3];
    println!("{frozen:?}");

    let mut thawed = frozen;
    thawed.push(4);
    println!("{thawed:?}");
}

We move a vector from an immutable to a mutable variable.

let mut thawed = frozen;
thawed.push(4);

The vector is moved from frozen to thawed. Because the new owner is mutable, we can append an element. The old variable can no longer be used. This is safe, since nobody else can observe the change. The same happens when a function takes a value by value and declares its parameter as mut.

$ cargo run -q
[1, 2, 3]
[1, 2, 3, 4]

Mutating an immutable collection

Methods like push take &mut self. We cannot call them on an immutable binding.

main.rs
fn main() {
    let numbers = Vec::new();
    numbers.push(1);

    println!("{numbers:?}");
}

The program does not compile.

let numbers = Vec::new();
numbers.push(1);

The compiler reports E0596: we cannot borrow numbers as mutable because it is not declared as mutable. A method that needs &mut self implicitly borrows the variable mutably, and a mutable borrow requires a mutable binding.

$ cargo run -q
error[E0596]: cannot borrow `numbers` as mutable, as it is not declared as mutable
 --> src/main.rs:3:5
  |
3 |     numbers.push(1);
  |     ^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut numbers = Vec::new();
  |         +++

For more information about this error, try `rustc --explain E0596`.

Mutable references

A mutable reference &mut T allows changing the value it points to. There are two different things that can be mutable: the binding that holds the reference and the value behind it.

main.rs
fn main() {
    let mut score = 10;
    let r = &mut score;
    *r += 5;
    println!("score = {score}");

    let a = 1;
    let b = 2;
    let mut pointer = &a;
    println!("pointer -> {pointer}");
    pointer = &b;
    println!("pointer -> {pointer}");
}

We use a mutable reference and a mutable binding holding a shared reference.

let mut score = 10;
let r = &mut score;
*r += 5;

The variable r is not declared mut, yet we can change the value through it. The reference itself never changes; it always points to score. We dereference it with *. While r is alive, score cannot be used; the borrow ends after the last use of r, so we can print score afterwards.

let mut pointer = &a;
pointer = &b;

The opposite case: pointer is a mutable binding, so it can point to something else, but it is a shared reference, so the numbers it points to cannot be changed. The two keywords are independent: let mut r = &mut x allows both.

$ cargo run -q
score = 15
pointer -> 1
pointer -> 2

Changing a value through a shared reference

A shared reference does not allow modification, even when the original variable is mutable.

main.rs
fn add_item(items: &Vec<i32>) {
    items.push(1);
}

fn main() {
    let mut items = vec![0];
    add_item(&items);
    items.push(2);

    println!("{items:?}");
}

The function takes the vector by shared reference and tries to change it.

fn add_item(items: &Vec<i32>) {
    items.push(1);
}

The compiler reports E0596: items is a & reference, so it cannot be borrowed as mutable. The signature is a promise to the caller that the function only reads the data. To modify the vector, the parameter must be declared as &mut Vec<i32> and the caller must pass &mut items. In practice, a function that only reads should take a slice, &[i32].

$ cargo run -q
error[E0596]: cannot borrow `*items` as mutable, as it is behind a `&` reference
 --> src/main.rs:2:5
  |
2 |     items.push(1);
  |     ^^^^^ `items` is a `&` reference, so it cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
1 | fn add_item(items: &mut Vec<i32>) {
  |                     +++

For more information about this error, try `rustc --explain E0596`.

Many readers or one writer

The borrowing rules say that at any time we can have either any number of shared references or exactly one mutable reference to a value. A borrow lasts from its creation to its last use, not to the end of the block.

main.rs
fn main() {
    let mut data = vec![1, 2, 3];

    let first = &data[0];
    let last = &data[2];
    println!("first = {first}, last = {last}");

    let all = &mut data;
    all.push(4);
    println!("{all:?}");
}

We take two shared references and then a mutable one.

let first = &data[0];
let last = &data[2];
println!("first = {first}, last = {last}");

Both shared references can exist at the same time, because nobody can change the vector while they are alive.

let all = &mut data;
all.push(4);

After the last use of first and last their borrows are over, so the mutable borrow is allowed. This is called non-lexical lifetimes. The compiler analyses where each reference is used, not where its variable goes out of scope.

$ cargo run -q
first = 1, last = 3
[1, 2, 3, 4]

Two mutable references

Two mutable references to the same value would be aliases that could change the value independently. Rust does not allow it.

main.rs
fn main() {
    let mut value = 100;

    let r1 = &mut value;
    let r2 = &mut value;

    *r1 += 1;
    *r2 += 1;
    println!("{value}");
}

We create two mutable references to value.

let r1 = &mut value;
let r2 = &mut value;

The compiler reports E0499: we cannot borrow value as mutable more than once at a time. It marks the first borrow, the second borrow, and the later use of the first one, which makes the borrows overlap. If we used r1 before creating r2, the program would compile.

$ cargo run -q
error[E0499]: cannot borrow `value` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut value;
  |              ---------- first mutable borrow occurs here
5 |     let r2 = &mut value;
  |              ^^^^^^^^^^ second mutable borrow occurs here
6 |
7 |     *r1 += 1;
  |     -------- first borrow later used here

For more information about this error, try `rustc --explain E0499`.

Mixing shared and mutable references

A mutable operation is also not allowed while a shared reference is alive.

main.rs
fn main() {
    let mut data = vec![1, 2, 3];

    let first = &data[0];
    data.push(4);

    println!("first = {first}");
}

We keep a reference to the first element and then append to the vector.

let first = &data[0];
data.push(4);
println!("first = {first}");

The compiler reports E0502. The restriction has a good reason: push may need to allocate a bigger buffer and move the elements. The reference first would then point to freed memory. In C++, this kind of bug is called iterator invalidation. Rust rejects it at compile time, because the shared borrow is still used by the last println!.

$ cargo run -q
error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let first = &data[0];
  |                  ---- immutable borrow occurs here
5 |     data.push(4);
  |     ^^^^^^^^^^^^ mutable borrow occurs here
6 |
7 |     println!("first = {first}");
  |                        ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.

Methods: &self, &mut self, and self

A method declares in its first parameter how it uses the instance: &self reads it, &mut self changes it, and self consumes it.

main.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Self {
        Counter { count: 0 }
    }

    fn get(&self) -> u32 {
        self.count
    }

    fn increment(&mut self) {
        self.count += 1;
    }

    fn into_total(self) -> u32 {
        self.count
    }
}

fn main() {
    let mut counter = Counter::new();
    println!("Initial: {}", counter.get());

    counter.increment();
    counter.increment();
    println!("After two increments: {}", counter.get());

    let total = counter.into_total();
    println!("Total: {total}");
}

The Counter struct has one method of each kind.

fn get(&self) -> u32 {
    self.count
}

The get method borrows the counter immutably. It can be called on any binding, mutable or not, and while other shared references exist.

fn increment(&mut self) {
    self.count += 1;
}

The &mut self syntax is shorthand for self: &mut Self. The method can change the fields, and the caller needs a mutable binding.

fn into_total(self) -> u32 {
    self.count
}

The self parameter takes ownership. After into_total the counter is gone, and the compiler does not allow its further use. Names starting with into_ conventionally denote such methods. See the structs tutorial for more about methods.

$ cargo run -q
Initial: 0
After two increments: 2
Total: 2

Calling a mutating method on an immutable binding

The caller needs a mutable binding to call a method with &mut self.

main.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn increment(&mut self) {
        self.count += 1;
    }
}

fn main() {
    let counter = Counter { count: 0 };
    counter.increment();

    println!("{}", counter.count);
}

We call increment on an immutable variable.

let counter = Counter { count: 0 };
counter.increment();

The compiler reports E0596 again, with the same suggestion: declare the variable as let mut counter.

$ cargo run -q
error[E0596]: cannot borrow `counter` as mutable, as it is not declared as mutable
  --> src/main.rs:13:5
   |
13 |     counter.increment();
   |     ^^^^^^^ cannot borrow as mutable
   |
help: consider changing this to be mutable
   |
12 |     let mut counter = Counter { count: 0 };
   |         +++

For more information about this error, try `rustc --explain E0596`.

Mutability of struct fields

In Rust, fields have no mut keyword of their own. The mutability of the whole struct is determined by the binding.

main.rs
#[derive(Debug)]
struct User {
    name: String,
    logins: u32,
}

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        logins: 0,
    };

    user.logins += 1;
    user.name.push_str(" Smith");

    let name = &user.name;
    user.logins += 1;
    println!("{name} has logged in {} times", user.logins);

    println!("{user:?}");
}

We change both fields of a mutable struct.

user.logins += 1;
user.name.push_str(" Smith");

Because user is mutable, all its fields are mutable. A field that is a String can be modified with its methods.

let name = &user.name;
user.logins += 1;
println!("{name} has logged in {} times", user.logins);

The compiler tracks borrows of individual fields. We hold a shared reference to name and change logins at the same time, which is fine because the fields do not overlap. This works only when the fields are accessed directly; a method call such as user.get_name() borrows the whole struct.

$ cargo run -q
Alice Smith has logged in 2 times
User { name: "Alice Smith", logins: 2 }

Assigning to a field of an immutable struct

If the binding is not mutable, no field can be changed.

main.rs
struct User {
    name: String,
    logins: u32,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        logins: 0,
    };

    user.logins += 1;

    println!("{} {}", user.name, user.logins);
}

We assign to a field of an immutable struct.

let user = User { ... };
user.logins += 1;

The compiler reports E0594: it cannot assign to user.logins, as user is not declared as mutable. To forbid changes of only some of the fields, we make them private and offer methods for the allowed changes.

$ cargo run -q
error[E0594]: cannot assign to `user.logins`, as `user` is not declared as mutable
  --> src/main.rs:12:5
   |
12 |     user.logins += 1;
   |     ^^^^^^^^^^^^^^^^ cannot assign
   |
help: consider changing this to be mutable
   |
 7 |     let mut user = User {
   |         +++

For more information about this error, try `rustc --explain E0594`.

Shadowing versus mutation

Shadowing declares a new variable with the same name using let. It is different from mutation: the new variable can have another type and the old one is not changed, only hidden.

main.rs
fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("spaces = {spaces}");

    let x = 5;
    {
        let x = x * 2;
        println!("inner x = {x}");
    }
    println!("outer x = {x}");

    let mut y = 5;
    {
        y *= 2;
        println!("inner y = {y}");
    }
    println!("outer y = {y}");
}

We compare shadowing with mutation, also in an inner block.

let spaces = "   ";
let spaces = spaces.len();

The first spaces is a string, the second one a number. With a mutable variable this would be a type error; with shadowing it is fine. The variable stays immutable after the transformation.

let x = 5;
{
    let x = x * 2;
    println!("inner x = {x}");
}
println!("outer x = {x}");

The inner x shadows the outer one only inside the block. After the block, the outer variable is visible again with its original value, 5.

let mut y = 5;
{
    y *= 2;
}
println!("outer y = {y}");

Mutation changes the variable itself, so the change is visible after the block: y is 10. Use shadowing for transformations of a value, and mut when a value is updated repeatedly, for example in a loop.

$ cargo run -q
spaces = 3
inner x = 10
outer x = 5
inner y = 10
outer y = 10

Mutable parameters

A function parameter is a binding, so it can be declared mut. This is different from a parameter of type &mut T.

main.rs
fn double_copy(mut n: i32) -> i32 {
    n *= 2;
    n
}

fn double_in_place(n: &mut i32) {
    *n *= 2;
}

fn main() {
    let a = 5;
    let result = double_copy(a);
    println!("a = {a}, result = {result}");

    let mut b = 5;
    double_in_place(&mut b);
    println!("b = {b}");
}

Two functions double a number in different ways.

fn double_copy(mut n: i32) -> i32 {
    n *= 2;
    n
}

The mut n makes the local variable mutable. The caller passed a copy of the integer, so the original is not affected; a is still 5. The mut is an implementation detail and is not part of the function's type.

fn double_in_place(n: &mut i32) {
    *n *= 2;
}

The &mut i32 parameter changes the caller's variable. The caller must write &mut b at the call site, which makes the modification visible in the code. See the functions tutorial for more.

$ cargo run -q
a = 5, result = 10
b = 10

Mutable closures

A closure that changes a captured variable is itself mutable state, and we must declare it with let mut.

main.rs
fn main() {
    let mut count = 0;

    let mut increment = || {
        count += 1;
        println!("count = {count}");
    };

    increment();
    increment();

    println!("final count = {count}");
}

The closure increments a counter that it captures from the outer scope.

let mut increment = || {
    count += 1;
    println!("count = {count}");
};

The closure captures count by mutable reference, so it implements the FnMut trait, and calling it requires a mutable binding. While the closure is alive, count is mutably borrowed and cannot be read. After the last call, we can print the final value. See the closures tutorial.

$ cargo run -q
count = 1
count = 2
final count = 2

Mutating elements in a collection

To change the elements of a collection in a loop, we need mutable references to them. The iter_mut method, or a loop over &mut the collection, provides them.

main.rs
fn main() {
    let mut prices = vec![100, 250, 400];

    for price in prices.iter_mut() {
        *price += *price / 10;
    }
    println!("{prices:?}");

    for price in &mut prices {
        *price -= 5;
    }
    println!("{prices:?}");

    if let Some(last) = prices.last_mut() {
        *last = 0;
    }
    println!("{prices:?}");
}

We raise and lower prices in a vector.

for price in prices.iter_mut() {
    *price += *price / 10;
}

Each price is a &mut i32. We dereference it to read and write the value. The loop borrows the vector mutably, so we cannot touch the vector itself inside the loop.

for price in &mut prices {
    *price -= 5;
}

This is the shorter form of the same loop.

if let Some(last) = prices.last_mut() {
    *last = 0;
}

The last_mut method returns an Option with a mutable reference to the last element, or None for an empty vector. There are similar methods first_mut and get_mut.

$ cargo run -q
[110, 275, 440]
[105, 270, 435]
[105, 270, 0]

Interior mutability with Cell and RefCell

Sometimes we need to change a value although we have only a shared reference. The types Cell<T> and RefCell<T> provide interior mutability: the borrowing rules are applied at run time instead of at compile time. The Cell type moves values in and out and is meant for small Copy values. The RefCell type hands out references and is used for other types.

main.rs
use std::cell::{Cell, RefCell};

struct Stats {
    hits: Cell<u32>,
    log: RefCell<Vec<String>>,
}

impl Stats {
    fn record(&self, event: &str) {
        self.hits.set(self.hits.get() + 1);
        self.log.borrow_mut().push(event.to_string());
    }
}

fn main() {
    let stats = Stats {
        hits: Cell::new(0),
        log: RefCell::new(Vec::new()),
    };

    stats.record("start");
    stats.record("stop");

    println!("hits = {}", stats.hits.get());
    println!("log = {:?}", stats.log.borrow());
}

The record method takes &self, but it still changes the state.

self.hits.set(self.hits.get() + 1);

We read the number with get and store a new one with set. No reference to the content exists, so no borrowing problem can occur.

self.log.borrow_mut().push(event.to_string());

The borrow_mut method returns a guard that works like &mut Vec<String>. When the guard is dropped, the borrow ends. The stats variable in main is immutable. Interior mutability is used for caches, counters, and shared structures such as graphs; we usually combine it with Rc. Use it only when the ordinary borrowing rules are too restrictive.

$ cargo run -q
hits = 2
log = ["start", "stop"]

Runtime borrow checking

A RefCell keeps track of the active borrows. The try_borrow_mut method returns a Result instead of panicking when the cell is already borrowed.

main.rs
use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(vec![1, 2, 3]);

    {
        let reader = cell.borrow();
        println!("first element: {}", reader[0]);

        match cell.try_borrow_mut() {
            Ok(_) => println!("mutable borrow succeeded"),
            Err(e) => println!("mutable borrow failed: {e}"),
        }
    }

    match cell.try_borrow_mut() {
        Ok(mut writer) => {
            writer.push(4);
            println!("mutable borrow succeeded");
        }
        Err(e) => println!("mutable borrow failed: {e}"),
    }

    println!("{:?}", cell.borrow());
}

We try to get a mutable borrow while a shared one is alive.

let reader = cell.borrow();
match cell.try_borrow_mut() {
    Ok(_) => println!("mutable borrow succeeded"),
    Err(e) => println!("mutable borrow failed: {e}"),
}

While the reader guard exists, a mutable borrow fails. The inner block ends the guard, and the second attempt succeeds. This is the same rule as at compile time (many readers or one writer), but violations are reported when the program runs.

$ cargo run -q
first element: 1
mutable borrow failed: RefCell already borrowed
mutable borrow succeeded
[1, 2, 3, 4]

RefCell panics

The borrow and borrow_mut methods panic when the rules are broken.

main.rs
use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(vec![1, 2, 3]);

    let first = cell.borrow();
    println!("first element: {}", first[0]);

    cell.borrow_mut().push(4);
    println!("not reached");
}

We keep a shared borrow and request a mutable one.

let first = cell.borrow();
cell.borrow_mut().push(4);

The program compiles but panics with the message "RefCell already borrowed". This is the price of interior mutability: a mistake that the compiler would catch with ordinary references becomes a run-time failure. The thread ID in the message differs between runs.

$ cargo run -q
first element: 1

thread 'main' (83124) panicked at src/main.rs:9:10:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ echo $?
101

Global state: atomics and mutexes

A static item is immutable, but it can contain a type with interior mutability. For numbers we use atomic types; for other data a Mutex, which is also safe to share between threads.

main.rs
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};

static REQUESTS: AtomicUsize = AtomicUsize::new(0);
static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new());

fn handle(name: &str) {
    REQUESTS.fetch_add(1, Ordering::Relaxed);
    NAMES.lock().unwrap().push(name.to_string());
}

fn main() {
    handle("alice");
    handle("bob");
    handle("carol");

    println!("requests: {}", REQUESTS.load(Ordering::Relaxed));
    println!("names: {:?}", NAMES.lock().unwrap());
}

We count requests and remember their names in two statics.

static REQUESTS: AtomicUsize = AtomicUsize::new(0);
static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new());

Both constructors are const functions, so they can initialize a static. The statics are not declared mut.

REQUESTS.fetch_add(1, Ordering::Relaxed);
NAMES.lock().unwrap().push(name.to_string());

The fetch_add method increments atomically. The lock method returns a guard, after which the vector can be changed. The call to unwrap handles a poisoned mutex, which happens only when another thread panicked while holding the lock.

$ cargo run -q
requests: 3
names: ["alice", "bob", "carol"]

The static mut variable

A static mut variable is a global that can be modified directly, but every access is unsafe.

main.rs
static mut COUNTER: u32 = 0;

fn main() {
    COUNTER += 1;

    println!("done");
}

We increment a mutable static without an unsafe block.

static mut COUNTER: u32 = 0;
COUNTER += 1;

The compiler reports E0133: use of a mutable static is unsafe, because it can be modified by multiple threads, which causes data races. Even inside an unsafe block, creating references to a static mut is denied by default in the 2024 edition. Prefer atomics, a Mutex, or passing the state as a parameter.

$ cargo run -q
error[E0133]: use of mutable static is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     COUNTER += 1;
  |     ^^^^^^^ use of mutable static
  |
  = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

For more information about this error, try `rustc --explain E0133`.

Guidelines

These rules lead to code that is easy to read and check:

Source

The Rust Programming Language: Variables and Mutability

The Rust Programming Language: References and Borrowing

Module std::cell

In this article we have worked with mutability in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.