Vibe Engines
YouTube

Lesson 4.3 · 11 min

Lending the key

Most functions only want to look at a message, not own it. Rust lets you lend a value with &, and the owner keeps the key. This lesson also shows the one thing a lender cannot do while the loan is out.

Adds to Chatterboxreply() borrows the message.

Lending instead of giving

You have seen the key move (lesson 4.2). Handing a key over every time a function wants to look at something would be exhausting: you would have to get it back each time. The second option is to lend it. The owner keeps the key. The borrower gets a reference, written with &, which lets it look but not take, and certainly not clean up.

Look at how little changes in the code. The function says what it needs with &str ("a borrowed piece of text"), and the caller says it is lending with &msg.

Two functions borrow the message; main keeps it
fn shout(msg: &str) -> String {
msg.to_uppercase()
}
 
fn main() {
let msg = String::from("hello");
let loud = shout(&msg);
println!("{loud} / {msg}");
}
$ cargo run
HELLO / hello

The last line uses msg after shout has run. Under the rule from lesson 4.2 that would have been an error. It is fine here because shout only held a borrow. The key never left msg.

Ownership Stageshout(&msg) borrows; main keeps ownership

Step through the program. Solid arrows are ownership; dashed arrows are borrows. Use ← → or the slider.

fn shout(msg: &str) -> String {
msg.to_uppercase()
}
 
fn main() {
let msg = String::from("hello");
let loud = shout(&msg);
println!("{loud} / {msg}");
}
  1. Line 5. The program starts in main.
  2. Line 6. msg is made and owns the text.
  3. Line 7. shout(&msg) lends msg. The dashed arrow is a borrow: it points at the text but owns nothing. msg still holds the key. view: &str borrows bytes 0..5 of msg.
  4. Line 2. shout reads the borrowed text and builds a brand new String, HELLO, that it will hand back by return.
  5. Line 3. shout ends. The loan is over, and because the borrower never owned the text, nothing is cleaned up. The new String's key goes to loud.
  6. Line 8. Both names are valid, so this prints HELLO / hello. stdout: HELLO / hello

What a borrow costs you

A loan has one condition, and it is a natural one. While somebody is looking at a value, the owner may not change it. Imagine the owner rewriting the text while the borrower is mid-sentence. So long as there are shared borrowers, the room is read-only. You can have as many lookers as you like, but no editors.

How long is "while"? From the line where the reference is made to the last line where it is used. Not to the end of the block. Read this program and predict.

PredictChange the text while a reference is still used
fn main() {
let mut transcript = String::from("you: hi");
let view = &transcript;
transcript.push_str(" there");
println!("{view}");
}

Does this compile — and if it does, does it run?

  • It compiles and runs
  • Compile error E0502
  • It compiles, then panics
Reveal the answer

It does not compile: E0502. view is a borrow of transcript, and it is used again on the last line. push_str in between wants to change the text, which is not allowed while the loan is live.

error[E0502]: cannot borrow `transcript` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let view = &transcript;
  |                ----------- immutable borrow occurs here
4 |     transcript.push_str(" there");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{view}");
  |                ---- immutable borrow later used here
Borrow TimelineHow long does each borrow live?

Each bar is one borrow, drawn from its first use to its last. Scrub the line to see what is alive.

fn main() {
let mut transcript = String::from("you: hi");
let view = &transcript;
transcript.push_str(" there");
println!("{view}");
}
  • transcript: owner — owner, lines 1–6
  • transcript: view = &transcript — shared borrow, lines 3–5
  • transcript: push_str (changes it) — mutable borrow, lines 4–4

Each row is a value and each bar is a stretch of lines. The blue bar is the loan, drawn from first use to last use. The change on line 4 lands inside it, and that overlap is the error. Now the compiler's own words.

Compiler CoachThe compiler, translated

This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.

fn main() {
let mut transcript = String::from("you: hi");
let view = &transcript;
transcript.push_str(" there");
println!("{view}");
}
error[E0502]: cannot borrow `transcript` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let view = &transcript;
  |                ----------- immutable borrow occurs here
4 |     transcript.push_str(" there");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("{view}");
  |                ---- immutable borrow later used here

E0502 in the compiler error atlas →

  1. cannot borrow `transcript` as mutable because it is also borrowed as immutable: "mutable" means changing, "immutable" means just looking. You are changing something that is being looked at.
  2. immutable borrow occurs here points at &transcript on line 3: the loan begins.
  3. mutable borrow occurs here points at push_str on line 4: someone tries to change it while the loan is out.
  4. immutable borrow later used here points at line 5: this is the use that keeps the loan alive. Without it there would be no error.

Fixes that were compiled for real

  • Finish looking before you change — compiles. The last use of <code>view</code> is now before <code>push_str</code>, so the loan has ended. Free.
  • Look at transcript itself afterwards — compiles. Drop the reference and print the owner after the change. You are not borrowing it at all anymore.
  • Trap: using it again after the change — still E0502. Giving the reference a different name does not help if it is still used after the change. Only the last use counts, not what the name is.

Chatterbox v0.4

Now reply, a new shout and archive all borrow, and main still owns the one message. The new lines are marked. Note that the help text here is the short one; it grows in later lessons as commands are added.

src/main.rs: every function borrows
fn reply(msg: &str) -> String {
match msg {
"/help" => String::from("commands: /help, /quit"),
"/quit" => String::from("bye"),
_ => format!("echo: {msg}"),
}
}
 
fn shout(msg: &str) -> String {
msg.to_uppercase()
}
 
fn archive(msg: &str) {
println!("[archived] {msg}");
}
 
fn main() {
let msg = String::from("/help");
println!("{}", reply(&msg));
println!("{}", shout(&msg));
archive(&msg);
println!("still mine: {msg}");
}
$ cargo run
commands: /help, /quit
/HELP
[archived] /help
still mine: /help
Try itYour turn: look without taking

Write is_command, which borrows a message and says whether it starts with /. Text has a method starts_with(x) that answers exactly that: true when the text begins with x, and a single character goes in single quotes. After the call, the caller still owns the message.

Write is_command, which borrows a message and says whether it starts with /. Text has a method starts_with(x) that answers exactly that: true when the text begins with x, and a single character goes in single quotes. After the call, the caller still owns the message.

fn is_command(msg: &str) -> bool {
// true when the message starts with a slash
false
}
 
fn main() {
let msg = String::from("/help");
println!("{} {}", is_command(&msg), msg);
}

Hidden tests (4): empty_is_not_a_command plain_text_is_not slash_is_a_command works_on_a_borrowed_string

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does & in front of a value do to ownership?Reveal
Nothing. It lends a reference; the owner keeps the key and the value is not cleaned up when the borrower finishes.
How long does a borrow last?Reveal
From the line where the reference is made to the last line where it is used.
Why is changing a value while a shared borrow is still used refused?Reveal
The borrower is reading it. A change could pull the text out from under them, so the compiler gives E0502.

That is lesson 4.3. Tick it off so the course remembers where you are.

Learn Rust by building a chat app · Lesson 4.3 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026