Understanding Rust Ownership and Borrowing
Learn the ownership rule that makes Rust feel different: one owner for a value, borrowing through references, and the compiler checks that prevent invalid memory access.
Rust does not feel hard because the syntax is strange.
Rust feels hard because it forces you to answer a question many other languages hide.
That question is: who owns this data right now?
If you cannot answer that clearly, Rust will stop the program from compiling.
That is what Day 2 is about.
This is the lesson where Rust stops feeling like “new syntax” and starts feeling like a different model of programming.
When Rust rejects your ownership code, the compiler is not being stubborn.
It is usually preventing one of these bugs:
- reading freed memory
- freeing the same memory twice
- mutating data while something else is reading it
The Core Mental Model
Rust organizes many memory-safety rules around one idea:
A value has one owner.
Other code can temporarily borrow that value, but borrowing is tightly controlled.
If you keep that model in your head, most ownership errors stop looking random.
They become questions like these:
- did ownership move to a new variable?
- am I trying to use the old variable after the move?
- am I borrowing the value read-only or mutably?
- am I trying to mix incompatible borrows at the same time?
That is the real teaching spine for this lesson.
Why Rust Needs This Rule
Other languages solve memory management in different ways.
Some languages make you manage memory manually. That gives you speed, but it also gives you bugs like:
- memory leaks
- dangling pointers
- double free errors
Other languages use a garbage collector. That avoids many memory bugs, but cleanup happens later at runtime, not at the exact moment ownership becomes irrelevant.
Rust takes a different path.
It tracks ownership at compile time.
That gives you:
- memory safety
- predictable cleanup
- no garbage collector pauses
This matters in Solana too. On-chain programs and performance-sensitive clients do not benefit from vague memory behavior.
Start With The Difference Between Copy And Move
Not every assignment behaves the same way in Rust.
That is the first thing to understand.
Stack-friendly values are usually copied
Simple fixed-size values like integers are cheap to copy.
fn main() {
let x = 5;
let y = x;
println!("x: {}, y: {}", x, y);
}Both x and y are valid.
That is because i32 is a Copy type.
Rust can duplicate it cheaply, so assignment does not invalidate x.
Heap-backed values usually move
Now look at String.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // this does not compile
println!("{}", s2);
}This assignment moves ownership from s1 to s2.
After that move, s1 is no longer valid.
That is not arbitrary.
String owns heap data. If both variables thought they owned the same allocation, Rust would have to guess who should free it later. That is exactly the kind of ambiguity Rust refuses.
Stack And Heap Only Matter Because Ownership Behaves Differently There
You do not need a deep computer architecture lecture here.
You only need the practical distinction.
- stack data is fixed-size and cheap to copy
- heap data is dynamic and usually owned through a wrapper like
StringorVec<T>
That is why integers often copy cleanly while String often moves.
fn main() {
let amount: u64 = 5000;
let copied_amount = amount;
let wallet_name = String::from("treasury");
let moved_name = wallet_name;
println!("{}", copied_amount);
println!("{}", moved_name);
}The u64 copy is trivial.
The String move transfers ownership of heap-backed data.
The Three Ownership Rules
You can compress most of Day 2 into three rules.
Rule 1: every value has one owner
fn main() {
let name = String::from("validator");
println!("{}", name);
}Here, name owns the string.
Rule 2: ownership can move
fn main() {
let name = String::from("validator");
let next_name = name;
println!("{}", next_name);
}After the assignment, next_name owns the string and name does not.
Rule 3: when the owner goes out of scope, Rust drops the value
fn main() {
{
let message = String::from("temporary");
println!("{}", message);
}
// message is gone here
}Rust frees the owned data when the scope ends.
That is the cleanup model.
No manual free. No garbage collector. Clear ownership and deterministic scope-based cleanup.
Function Calls Also Move Ownership
Many beginners understand assignment moves, then get confused when the same thing happens in a function call.
A function parameter can also take ownership.
fn main() {
let signature = String::from("abc123");
consume_signature(signature);
// println!("{}", signature); // this does not compile
}
fn consume_signature(value: String) {
println!("Consumed: {}", value);
}consume_signature takes a String, not a reference.
That means the function becomes the new owner.
When the function ends, value goes out of scope and Rust drops it.
If you still need the original value afterward, do not move it. Borrow it instead.
Borrowing Lets You Use Data Without Taking Ownership
Moving every String into every function would be annoying and wasteful.
That is why Rust has references.
A reference lets you borrow data without becoming its owner.
Immutable borrowing
fn main() {
let wallet_label = String::from("treasury-wallet");
let length = label_length(&wallet_label);
println!("{} has length {}", wallet_label, length);
}
fn label_length(label: &String) -> usize {
label.len()
}label_length borrows the string with &String.
That means:
- the function can read the data
- the function does not own the data
- the original variable is still valid afterward
This is your first important ownership escape hatch.
Mutable Borrowing Means Temporary Exclusive Access
If a function needs to change the borrowed value, immutable borrowing is not enough.
Use a mutable reference.
fn main() {
let mut memo = String::from("pending");
append_status(&mut memo);
println!("{}", memo);
}
fn append_status(value: &mut String) {
value.push_str(" -> confirmed");
}Two things changed here.
- the owner,
memo, is declared withmut - the borrow uses
&mut
That is Rust being explicit again.
Mutation is allowed, but only when the code says so clearly.
The Borrowing Rule That Trips Up Most People
Rust allows either:
- many immutable references
- one mutable reference
But not both at the same time.
This is one of the most important rules in the language.
fn main() {
let mut state = String::from("ready");
let r1 = &state;
let r2 = &state;
println!("{} {}", r1, r2);
let r3 = &mut state;
r3.push_str(" -> sent");
println!("{}", r3);
}This works because the immutable borrows are finished before the mutable borrow begins.
Now look at the broken version:
fn main() {
let mut state = String::from("ready");
let r1 = &state;
let r2 = &mut state;
println!("{} {}", r1, r2);
}Rust rejects that code because one part of the program wants read access while another part wants exclusive write access.
That is exactly how data races start in less strict languages.
Rust stops it before the program runs.
Slices Are Borrowed Views Into Data
A slice is not a new owned value.
A slice is a borrowed view into part of existing data.
String slices
fn main() {
let phrase = String::from("solana runtime");
let first = &phrase[0..6];
let second = &phrase[7..14];
println!("{}", first);
println!("{}", second);
}Both first and second borrow part of phrase.
They do not own new strings.
That is why slices are efficient.
&str matters more than &String
When you only need read access to string data, prefer &str in function signatures.
fn main() {
let owned = String::from("rpc-node");
let literal = "validator";
println!("{}", describe(owned.as_str()));
println!("{}", describe(literal));
}
fn describe(value: &str) -> &str {
value
}&str is more flexible than &String.
It works with string literals and borrowed pieces of String values too.
That is the interface you usually want for read-only string input.
Clone Only When You Actually Need A Second Owned Copy
A lot of beginners overuse .clone() because it feels like an easy way out.
Sometimes it is correct. Often it is just hiding confusion.
fn main() {
let original = String::from("payer");
let copied = original.clone();
println!("{} {}", original, copied);
}This is legal because .clone() creates a real second owned string.
But cloning allocates and copies data.
So the default order of thinking should be:
- can I borrow this?
- if not, does moving ownership make sense?
- only then: do I truly need to clone?
That rule matters even more when data structures get bigger.
One Small Ownership Example That Looks Like Real Work
Now put the ideas together in a simple transaction-like example.
#[derive(Debug)]
struct TransferRequest {
from: String,
to: String,
amount: u64,
note: String,
}
fn main() {
let mut request = TransferRequest {
from: String::from("alice"),
to: String::from("bob"),
amount: 500_000,
note: String::from("pending"),
};
print_summary(&request);
mark_confirmed(&mut request);
print_summary(&request);
}
fn print_summary(request: &TransferRequest) {
println!(
"{} -> {} | amount: {} | note: {}",
request.from, request.to, request.amount, request.note
);
}
fn mark_confirmed(request: &mut TransferRequest) {
request.note = String::from("confirmed");
}What is happening here:
requestowns all its fieldsprint_summaryborrows it immutably because it only readsmark_confirmedborrows it mutably because it changesnote- ownership never moves out of
main, sorequestis still usable at the end
This is the kind of reasoning you need before structs and methods start feeling natural.
Common Ownership Mistakes
Using a value after it moved
If you assign a String to a new variable or pass it into a function that takes ownership, the original binding is no longer valid.
Do not fight that rule. Decide whether you actually wanted a borrow instead.
Mixing immutable and mutable borrows carelessly
If one part of the code is reading a value, Rust will not let another part mutate it at the same time.
That restriction prevents inconsistent state and data races.
Cloning when borrowing would be enough
.clone() is not evil, but it is not the default solution either.
If you only need temporary access, borrow the value.
Summary
Ownership is the rule that makes Rust feel different.
The short version is:
- one owner controls a value
- ownership can move
- borrows let other code use the value temporarily
- mutable borrows require exclusive access
- slices are borrowed views, not owned copies
If you can trace who owns the data and who is only borrowing it, Rust starts to make sense.
That is the capability you should leave Day 2 with.
The next lesson builds on this by introducing structs and methods, where ownership stops being a toy example and starts shaping real program design.