Editorâs note: This article was last updated by Eze Sunday on 25 March 2024 to cover advanced borrowing patterns in Rust, including mutable and immutable borrows, insights into the workings of the borrow checker within Rustâs concurrency model, and a brief comparison of memory management in garbage-collected languages and Rust.
Youâve heard a lot about it, youâve bought into the hype, youâve seen a couple of video tutorials and articles and now youâre finally ready to give Rust a try!
So you sit down, hands on the keyboard, write a few lines of code, and run the cargo run command, excited to see whether the program works as expected. Youâve heard that Rust is one of those languages that simply works once itâs compiled, giving you peace of mind. The compiler starts up, you follow the output, and then, suddenly you get the following error:
error[E0382]: borrow of moved value
Okay, I guess itâs not that simple. Weâve got one more concept to learn: the borrow checker â I sometimes refer to it as the memory police đ.
The borrow checker is an essential feature of the Rust language and part of what makes Rust Rust. It helps you (or forces you) to manage ownership. As chapter four of âThe Rust Programming Languageâ puts it, âOwnership is Rustâs most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector.â
In the last few sentences, weâve mentioned ownership, borrow checker, and garbage collectors. Thereâs a lot to unpack there, so letâs break it down a bit. In this guide, weâll look at what the borrow checker does for us (and what it stops us from doing), what guarantees it gives us, and how it compares to other forms of memory management.
Iâll assume that you have some experience writing code in higher-level languages such as Python, JavaScript, and C#, but not necessarily that youâre familiar with how computer memory works.
Letâs talk about memory and memory management for a minute. In most popular programming languages, you donât need to think about where your variables are stored. You simply declare them and the language runtime takes care of the rest via a garbage collector. This abstracts away how the computer memory actually works and makes it easier and more uniform to work with. This is a good thing.
However, we need to peel back a layer to show how this compares to the borrow checker. Weâll start by looking at the stack and the heap.
Your programs have access to two kinds of memory where they can store values: the stack and the heap. These differ in several ways, but for our sake, the most important difference is that data stored on the stack must have a known, fixed size. Data on the heap can be of any arbitrary size.
What do I mean by size? Size refers to how many bytes it takes to store the data. In broad terms, certain data types, such as Booleans, characters, and integers, have a fixed size. These are easy to put on the stack. On the other hand, data types such as strings, lists, and other collections can be of any arbitrary size. As such, they cannot be stored on the stack. We must instead use the heap.
Because data of arbitrary size can be stored on the heap, the computer needs to find a chunk of memory large enough to fit whatever weâre looking to store. This is time-consuming, and the program doesnât have direct access to the data as with the stack. Instead, itâs left with a pointer to where the data is stored.
A pointer is pretty much what it says on the tin: it points to some memory address on the heap where the data youâre looking for can be found. There are several pointer tutorials available on the web, and which one works for you depends on your background.
Whatâs the point of having these two different memory stores? Because of the way the stack works, data access on the stack is fast and easy but requires the data to conform to certain standards. The heap is slower but more versatile and is thus useful when you canât use the stack.
In garbage-collected languages, you donât need to worry about what goes on the stack and what goes on the heap. Data that goes on the stack gets dropped once it is out of scope. Data that lives on the heap is taken care of by the garbage collector once itâs no longer needed.
In languages such as C, on the other hand, you need to manage memory yourself. Where you might simply initialize a list in higher-level languages, you need to manually allocate memory on the heap in C. And when youâve allocated memory, you should also free the memory once youâre done with it to avoid memory leaks. But be careful: memory should only be freed once.
This process of manual allocation and freeing is error-prone. In fact, a Microsoft representative revealed that 70 percent of all of Microsoftâs vulnerabilities and exploits are memory-related.
So why would you use manual memory management? Because it allows for more control and offers better performance characteristics than garbage collection. The program doesnât need to stop what itâs doing and spend time finding out what it needs to clean up before cleaning it up.
Rustâs ownership model feels like something in between. By monitoring where data is used throughout the program and following a set of rules, the borrow checker can determine where data needs to be initialized and where it needs to be freed (or dropped, in Rust terms). Itâs like it auto-inserts memory allocations and frees it for you, giving you the convenience of a garbage collector with the speed and efficiency of manual management.
In practice, this means that, when passing variables around, you can do one of three things. You can move the data itself and give up ownership in the process, create a copy of the data and pass that along, or pass a reference to the data and retain ownership, letting the recipient borrow it for a while. The most appropriate approach depends entirely on the situation.
At a glance, here is a table that summarizes the comparison between garbage-collected languages and the Rust memory management with the ownership model:
| Feature | Rust ownership model | Garbage collection |
|---|---|---|
| Memory deallocation | Deterministic at compile time. Values are dropped when they go out of scope | Non-deterministic, typically during runtime pauses |
| Memory safety | Prevents memory errors (use-after-free, dangling pointers, etc.) at compile time | Relies on runtime checks to prevent memory errors |
| Performance overhead | Minimal runtime overhead. No background garbage collection processes | Can introduce runtime pauses and overhead due to tracing and collection |
| Predictability | Predictable memory usage and deallocation timing | Less predictable memory usage patterns, potential for runtime pauses |
| Development complexity | Requires understanding ownership and borrowing, can have a steeper learning curve | Often simpler for developers, as memory management is abstracted away |
A data race occurs when two or more threads try to access and potentially modify the same memory location simultaneously, without any synchronization mechanisms.
In addition to handling memory allocation and freeing for the programmer, the borrow checker also prevents data races through its set of sharing rules. These same borrowing rules also help you work with concurrent and parallel code without having to worry about memory safety, enabling Rustâs fearless concurrency.
Here is a simple example of one way Rust handles that concurrency:
use std::sync::Mutex;
use std::thread;
let counter = Mutex::new(0);
fn increment() {
for _ in 0..10000 {
let mut data = counter.lock().unwrap();
*data += 1;
}
}
fn main() {
let t1 = thread::spawn(increment);
let t2 = thread::spawn(increment);
t1.join().unwrap();
t2.join().unwrap();
println!("Counter: {}", counter.lock().unwrap());
}
In the above code:
counter variable is protected within a Mutex. A Mutex is a lock that allows only one thread to access a shared resource at a timelock(): Before each thread in the forâloop can modify counter, it must acquire a lock using counter.lock(). This ensures that only one thread has mutable access to the counter at any given momentAs with all good things in life, Rustâs ownership system comes with its fair share of drawbacks.
The borrow checker can be tricky to understand and work with â so much so that itâs pretty common for newcomers to the Rust community to get stuck fighting it. Iâve personally lost many hours of my life to this struggle.
For instance, sharing data can suddenly become a problem, especially if you need to mutate it at the same time. Certain data structures that are easy to create from scratch in other languages are hard to get right in Rust. For a good example, check out the book âLearn Rust With Entirely Too Many Linked Lists.â It goes through several ways to implement a linked list in Rust and details all the issues the author ran into on the way there. Itâs both informative and very entertaining â well worth a read.
Once you get on board with the borrow checker, things start to improve. I quite like this Reddit userâs explanation:
[The borrow checker] operates by a few simple rules. If you donât understand or at least have some intuition for what those rules are, then itâs going to be about as useful as using a spell checker to help you write in a language you donât even know: itâll just reject everything you say. Once you know the rules the borrow checker is based on, youâll find it useful rather than oppressive and annoying, just like a spell checker.
And what are these rules? Here are the two most important ones to remember concerning variables that are stored on the heap:
Next, letâs take a closer look at the mutable and immutable borrows.
By default, Rust variables are immutably borrowed â simply put, this means you canât change the value of a variable after its initial assignment unless you explicitly mark it as mutable, which makes it a mutable borrow.
Here is a simple example of an immutable borrow attempt:
fn main(){
let x = 5;
x = 6; // Error! Immutable variable
println!("X is: {}", x);
}
The above will throw an error because we are attempting to update a memory location that has been marked unchangeable (immutable). The error message will look like this:
error[E0384]: cannot assign twice to immutable variable `x` --> src/main.rs:4:5 | 3 | let x = 5; | - | | | first assignment to `x` | help: consider making this binding mutable: `mut x` 4 | x = 6; // Error! Immutable variable | ^^^^^ cannot assign twice to immutable variable For more information about this error, try `rustc --explain E0384`.
The interesting thing about Rust is that it gives you suggestions on how to fix the problems the compiler throws at you. From the code above, itâs suggesting we add mut to the variable, which is exactly how to make a value mutable in Rust.
So, weâll make an update to our code by adding mut to the X variable as shown below:
fn main(){
let mut x = 5;
x = 6;
println!("X is: {}", x);
}
Now, the result for the code above will be X is: 6, the value of X has changed from 5 to the new value assigned to it â 6.
Now that you have some understanding of what the borrow checker is and how it works, letâs examine how it affects us in practice. Weâll be working with the Vec<T> type, which is Rustâs version of a growable list (analogous to Pythonâs lists or JavaScriptâs arrays). Because it doesnât have a fixed size, a Vec needs to be heap-allocated.
The example may be contrived, but it demonstrates the basic principles. Weâll create a vector, call a function that simply accepts it as an argument, and then try and see whatâs inside.
Note: This code sample does not compile:
fn hold_my_vec<T>(_: Vec<T>) {}
fn main() {
let v = vec![2, 3, 5, 7, 11, 13, 17];
hold_my_vec(v);
let element = v.get(3);
println!("I got this element from the vector: {:?}", element);
}
When trying to run this, youâll get the following compiler error:
error[E0382]: borrow of moved value: `v`
--> src/main.rs:6:19
|
4 | let v = vec![2, 3, 5, 7, 11, 13, 17];
| - move occurs because `v` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
5 | hold_my_vec(v);
| - value moved here
6 | let element = v.get(3);
| ^ value borrowed here after move
The above message tells us that Vec<i32> doesnât implement the Copy trait and, as such, must be moved (or borrowed). The Copy trait is only implementable by data types that can be put on the stack, and because Vec must go on the heap, it cannot implement Copy. We need to find another way around this.
Clone traitEven though a Vec canât implement the Copy trait, it can (and does) implement the Clone trait. In Rust, cloning is another way to make duplicates of data. But while copying can only be done on stack-based values and is always very cheap, cloning also works on heap-based values and can be very expensive.
So if the function takes ownership of the value, why donât we just give it a clone of our vector? Thatâll make it happy, right? Indeed, the below code works just fine:
fn hold_my_vec<T>(_: Vec<T>) {}
fn main() {
let v = vec![2, 3, 5, 7, 11, 13, 17];
hold_my_vec(v.clone());
let element = v.get(3);
println!("I got this element from the vector: {:?}", element);
}
However, weâve now done a lot of extra work for nothing. The hold_my_vec function doesnât even use the vector for anything; it just takes ownership of it. In this case, our vector (v) is pretty small, so itâs not a big deal to clone it. In the just-getting-things-to-work stage of development, this may be the quickest and easiest way to see results.
However, there is a better, more idiomatic way to do this. Letâs have a look.
As mentioned previously, rather than giving away our variable to the other function, we can lend it to them. To do this, we need to change the signature of hold_my_vec to instead accept a reference by changing the type of the incoming parameter from Vec<T> to &Vec<T>.
We also need to change how we call the function and let Rust know that weâre only giving the function a reference â a borrowed value. This way, we let the function borrow the vector for a little bit but ensure that we get it back before continuing the program:
fn hold_my_vec<T>(_: &Vec<T>) {}
fn main() {
let v = vec![2, 3, 5, 7, 11, 13, 17];
hold_my_vec(&v);
let element = v.get(3);
println!("I got this element from the vector: {:?}", element);
}
This is only a very brief overview of the borrow checker, what it does, and why it does it. A lot of the finer details have been left out to make this tutorial as easy to digest as possible.
Often, as your programs grow, youâll find more intricate problems that require more thinking and fiddling with ownership and borrows. You may even have to rethink how youâve structured your program to make it work with Rustâs borrow checker.
Itâs a learning curve, but if you stick around and make your way to the top, youâre sure to have learned a thing or two about memory along the way.
To further solidify your knowledge of the Rust borrow checker, I recommend the following articles:
Happy hacking!
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If youâre interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings â compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps â start monitoring for free.

Can AI refactor legacy CSS without breaking your layout? We benchmarked 5 top AI assistants across cascade order, specificity ties, and stacking traps.

Learn how to offload long-running Gemini AI requests to Trigger.dev background jobs in Next.js using Server Actions and real-time React hooks.

Learn how to use Google’s LiteRT.js to build a browser-based OCR receipt scanner with WebGPU acceleration and on-device LLM structuring via LiteRT-LM.

Learn how to use the TypeScript Compiler API and AST traversal to extract imports and build a file dependency graph CLI.
Hey there, want to help make our blog better?
Join LogRocketâs Content Advisory Board. Youâll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now