Ownership and Borrowing Practice
Practice Rust ownership with three focused exercises that make you distinguish copying, moving, immutable borrowing, mutable borrowing, and when cloning is actually necessary.
Ownership only becomes real when the compiler starts rejecting code you thought was fine.
That is what this page is for.
These exercises are designed to force one decision again and again:
Should this value move, be borrowed, or be cloned?
If you can answer that clearly, this lesson did its job.
Do these in order.
The first exercise is read-only borrowing. The second adds mutation. The third makes you debug ownership decisions directly.
What You Are Practicing
By the end of this page, you should be able to:
- tell when assignment copies and when it moves
- pass borrowed data into functions without losing the original owner
- use
&mutonly when a function really needs to change data - prefer borrowing over cloning when borrowing is enough
- read basic ownership compiler errors without panicking
If any of those still feels fuzzy, that is the exact reason to do the exercises carefully.
Exercise 1: Borrow A Transfer Note Without Moving It
Start with the simplest useful ownership decision.
You have a String containing a transfer note. You want to inspect it in a helper function, but you still need to use the original value afterward.
That means this exercise is about borrowing, not moving.
What to build
Create a new Cargo project:
cargo new borrow_note
cd borrow_noteReplace src/main.rs with this starter:
fn main() {
let note = String::from("rent payment for validator account");
let word_count = count_words(¬e);
let first_word = first_word(¬e);
println!("Word count: {}", word_count);
println!("First word: {}", first_word);
// `note` should still be usable here.
println!("Original note: {}", note);
}
fn count_words(note: &str) -> usize {
// write this
0
}
fn first_word(note: &str) -> &str {
// write this
""
}Why this exercise matters
It proves that you understand three important ideas:
- a helper function does not need ownership if it only reads data
&stris a better read-only interface than&String- slices let you return part of the original string without allocating a new one
Your job
- implement
count_words - implement
first_word - run
cargo run - confirm that
noteis still usable after both function calls - change the note text and predict the output before rerunning
What success looks like
You should be able to explain:
- why
notewas borrowed instead of moved - why
first_wordreturns&strinstead ofString - why returning a slice is cheaper than cloning a new string
Exercise 2: Update Transaction State With A Mutable Borrow
Now move from read-only borrowing to mutation.
You have a transfer request that starts as pending. A helper function should change it to confirmed.
That means the function needs temporary exclusive access.
What to build
Use this starter code:
#[derive(Debug)]
struct TransferRequest {
from: String,
to: String,
amount: u64,
status: String,
}
fn main() {
let mut request = TransferRequest {
from: String::from("alice"),
to: String::from("bob"),
amount: 250_000,
status: String::from("pending"),
};
print_summary(&request);
mark_confirmed(&mut request);
print_summary(&request);
}
fn print_summary(request: &TransferRequest) {
println!(
"{} -> {} | amount: {} | status: {}",
request.from, request.to, request.amount, request.status
);
}
fn mark_confirmed(request: &mut TransferRequest) {
// write this
}Why this exercise matters
This is the ownership rule that beginners usually understand in theory but misuse in code.
print_summaryonly reads, so it should borrow immutablymark_confirmedchanges the struct, so it needs&mutrequestitself must be declared withmutbecause the owned value is changing
Your job
- implement
mark_confirmed - run the program and confirm the second print shows
confirmed - add another helper like
mark_failedormark_sent - explain why Rust would reject this if an immutable borrow and mutable borrow overlapped incorrectly
What success looks like
You should be able to say this cleanly:
- immutable borrows are for reading
- mutable borrows are for changing
- one mutable borrow needs exclusive access while it is active
Exercise 3: Fix Move Errors Without Reaching For clone() First
Now practice the part that actually trips people up.
The code below is broken on purpose.
Your job is not just to make it compile. Your job is to choose the right fix.
Broken program
fn main() {
let wallet_name = String::from("treasury-wallet");
print_label(wallet_name);
println!("Wallet still available: {}", wallet_name);
}
fn print_label(label: String) {
println!("Label: {}", label);
}What is wrong
wallet_name moves into print_label because the function takes ownership with String.
That means the final println! tries to use a value that no longer belongs to main.
Your job
Fix this in the best way.
The correct first fix is to change the function so it borrows the string:
fn print_label(label: &str) {
println!("Label: {}", label);
}And then call it like this:
print_label(&wallet_name);After that works, try two more experiments:
- intentionally use
.clone()instead and explain why it works but is not the best default - write a second function that really should take ownership, such as one that returns the string uppercased after consuming it
What success looks like
You should be able to explain the difference between these three choices:
- borrow when the function only needs temporary read access
- move when ownership transfer is the point
- clone only when you truly need a second owned copy
That decision process is the real exercise.
How To Work Through These Exercises
Read the function signatures first
Before changing any code, inspect the function parameter types.
Ownership problems usually start there.
Ask what the function actually needs
Does it only read the data?
Then borrow it.
Does it need to change the data?
Then use &mut.
Does it need to become the new owner?
Then moving may be correct.
Treat clone() as a deliberate cost
Do not use .clone() as your first reflex.
Use it only after you can explain why borrowing is not enough.
Common Failure Modes
Returning an owned String when a slice would do
If you only need to point at part of existing string data, return &str instead of allocating a new String.
Using &String where &str is better
For read-only string input, &str is usually the cleaner and more flexible function signature.
Using clone() to hide confusion
A clone may compile, but that does not mean it was the right ownership choice.
First ask whether borrowing already solves the problem.
Summary
These exercises were built to make ownership concrete.
If you worked through them honestly, you now have hands-on practice with:
- immutable borrowing
- mutable borrowing
- slices
- move errors
- the difference between borrowing, moving, and cloning
That is the practical core of this lesson.
The next lesson moves into structs and methods, where these same ownership decisions start showing up inside real data models instead of tiny standalone examples.