Skip to content

References & Borrowing

Moving a value into a function is often unnecessary — you just want the function to read the value, not own it. Rust solves this with references. A reference lets you pass a value to a function without giving up ownership.

The syntax &T means “a shared reference to a value of type T”. Creating a reference is called borrowing.

fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it does not own the data, nothing is dropped
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // pass a reference, not the value
println!("{} has {} characters", s, len); // s is still valid
}

The &s syntax creates a reference to s. The function signature s: &String declares that the parameter is a reference, not an owned String.

Because a reference does not own the value it points to, the value is not dropped when the reference goes out of scope. Ownership stays with the original variable.

fn print_greeting(name: &String) {
println!("Hello, {}!", name);
} // reference dropped here — name itself is unaffected
fn main() {
let name = String::from("Alice");
print_greeting(&name);
print_greeting(&name); // can borrow again — we still own name
println!("Still have: {}", name);
}

You can have any number of shared (&T) references to a value at the same time, as long as none of them are mutable references. Shared references are read-only.

fn main() {
let s = String::from("shared");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // all three are valid simultaneously
}

When a function parameter is &String, the caller passes &my_string. When the parameter is &str (a string slice), the function is more flexible — it accepts both &String and string literals. Prefer &str over &String for function parameters as a general rule.

fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let owned = String::from("Bob");
greet(&owned); // &String coerces to &str
greet("Charlie"); // string literal is already &str
}

&str vs &String vs String: Which One Do I Write?

Section titled “&str vs &String vs String: Which One Do I Write?”

Three text types show up in almost every Rust signature, and mixing them up is the single most common source of unnecessary .clone()s and .to_string()s in AI-generated code. The choice comes down to one question: does this function need to own the text afterward?

flowchart TD
  Q{Does the function need to own the text afterward?}
  Q -->|Yes - store it, move it into a struct, send it elsewhere| OWN[Parameter type: String]
  Q -->|No - it only reads the text| REF[Parameter type: &str]
  REF -->|Caller has a String| COERCE1[Pass &my_string - auto-coerces to &str]
  REF -->|Caller has a literal| COERCE2[Pass the literal directly - it is already &str]
Choosing a text parameter type
SituationUseWhy
Function only reads the text&strAccepts both &String (via coercion) and string literals — the most flexible input type
Function must keep, store, or move the text elsewhereStringTaking ownership is unavoidable if the value needs to outlive the call
Returning a view into data the caller already owns&str tied to an input’s lifetimeNo allocation; the returned slice borrows from the input
Returning brand-new text built inside the functionStringYou cannot return a reference to data that lives only inside the function (see below)
A function parameter written as &StringAlmost never the right choiceIt is strictly less flexible than &str for no benefit — always prefer &str

Returning References and a Lifetime Teaser

Section titled “Returning References and a Lifetime Teaser”

Taking a reference as a parameter is straightforward — the compiler already knows the reference cannot outlive its input. Returning a reference is where it gets interesting: the compiler needs to know which input the returned reference is allowed to borrow from, because different call sites might pass arguments with different lifespans.

// @expect-error E0106
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!");
println!("{}", longest(&s1, &s2));
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++

The fix is exactly what the compiler suggests: a named lifetime parameter, 'a, that ties the return value’s validity to both inputs at once.

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!");
println!("longest: {}", longest(&s1, &s2));
}

<'a> does not change how long anything lives — it is a promise, checked at compile time, that the returned &str will not outlive whichever of a or b is shorter-lived. This is only a teaser: the full set of elision rules, 'static, and lifetimes on structs get their own lesson in Lifetimes.

fn calculate_length(s: &String) -> usize {
s.len()
}
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let s = String::from("hello");
// Pass a reference — s is not moved
let len = calculate_length(&s);
println!("length of {} is {}", s, len);
// Multiple shared borrows are allowed
let r1 = &s;
let r2 = &s;
println!("r1={} r2={}", r1, r2);
// &String coerces to &str
let owned = String::from("Alice");
greet(&owned); // borrow, not move
greet("Bob"); // string literal
println!("owned is still: {}", owned);
}

A reference is a promise, not a pointer with extra syntax: &T says “let me look at this value, but I will never outlive it.” The compiler tracks, for every reference, exactly which owned value it points to and refuses to compile any path where the reference could still be read after that owner is gone. Returning a reference just makes that promise explicit in the function signature — a lifetime parameter names whose promise the return value inherits.

Borrowing avoids an allocation and a copy — &str/&String cost nothing beyond a pointer and a length. The price is that a borrowed value’s lifetime becomes part of your function’s public contract: sometimes the compiler cannot express what you want (a function that sometimes returns a reference and sometimes needs to construct new text, for instance), and the honest fix is to return an owned String instead of fighting the borrow checker with lifetime annotations. Knowing when to stop borrowing and start owning is itself a skill — the decision table above is the fast path for parameters; return types often need the same judgment call.

  • “A reference is basically a raw pointer, so fn f() -> &String should work like returning a pointer does in C.” In C, returning a pointer to a local is a silent bug (a dangling pointer, undefined behavior on use). In Rust it does not compile at all — E0106/E0515-style errors exist specifically to catch this at compile time, because the compiler has nowhere valid to say the reference is “borrowed from.”
  • “&String and &str are interchangeable, so it doesn’t matter which one I write in a signature.” They coerce for the caller’s convenience, but as a parameter type &str is strictly more flexible — it accepts literals that &String cannot — with no downside. There is essentially never a reason to write &String in a signature.
  • “Since I can have many shared references at once, I should be able to write through one of them too.” &T is read-only no matter how many references exist — the exclusivity rule that lets you write through a reference (&mut T) is a completely separate, stricter guarantee, covered next in Mutable Borrows.

AI assistants frequently write a function parameter as &Vec<T> (a reference to an owned, growable vector) when the function only ever iterates or indexes — &[T], a slice, does the same job while also accepting arrays and any partial slice of a vector.

🐛 Spot the bug

fn sum_all(v: &Vec<i32>) -> i32 {
v.iter().sum()
}
fn main() {
let numbers = vec![1, 2, 3, 4];
println!("{}", sum_all(&numbers));
}
Show the answer

This compiles and runs fine — the bug is a flexibility smell, not a compile error. &Vec<i32> only accepts a reference to an actual Vec<i32>; it rejects an array ([i32; 4]) and any already-sliced portion of a vector (&numbers[1..3]) without an extra explicit re-slice. Changing the parameter to &[i32] accepts all of those automatically via coercion, with the exact same function body — the Slices lesson covers why &[T]/&str generalize &Vec<T>/&String this way.

What does the & symbol do when placed before a type in a function parameter?
What happens to a String when a function that takes &String returns?
How many shared (&T) references to the same value can exist simultaneously?
Why should function parameters prefer &str over &String?
Does this compile?
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}
What does this program print?
fn greet(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    let owned = String::from("Bob");
    greet(&owned);
    greet("Charlie");
}