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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
#[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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
- Start with
letand addmutonly when the compiler requires it. A reader then knows which values change. - Prefer creating new values (with shadowing or transformations) to changing old ones.
- Take
&Tor&[T]in functions that only read and&mut Tin functions that change their argument. - Keep mutable borrows short. The shorter the borrow, the fewer conflicts with other references.
- Use
Cell,RefCell, atomics, andMutexonly when shared access must allow changes, and know that the checks happen at run time. - Avoid
static mut.
Source
The Rust Programming Language: Variables and Mutability
The Rust Programming Language: References and Borrowing
In this article we have worked with mutability in Rust.
Author
List all Rust tutorials.