The house and the key
You write a function to process a user's profile. You pass the profile data in. The function extracts a username and returns it. Back in main, you try to print the original profile data. The compiler rejects you with E0382 (use of moved value). You didn't lose the data; you handed it over.
Or you try to return a reference to a local variable. The compiler rejects you with E0515 (cannot return reference to local variable). You're trying to hand out a key to a house that gets demolished the moment the function returns.
These errors all trace back to the same distinction. Rust separates data into two categories: owned types and borrowed types. Owned types hold the resource. Borrowed types point to a resource held by someone else. Understanding the difference is the key to writing Rust that compiles, runs fast, and never leaks memory.
Ownership is a deed, borrowing is a key
Think of a String like a house. When you create a String, you buy the house. You hold the deed. You can live in it, renovate it, sell it, or demolish it. When you're done, you can tear it down and reclaim the land. The String owns the bytes on the heap. It is responsible for cleaning them up.
A &str is like a key to that house. You can use the key to enter and look around. You can read the contents. You can't sell the house. You can't demolish it. If the owner sells the house to someone else, your key becomes useless. The &str borrows the data. It relies on the owner to keep the data alive.
Rust enforces this at compile time. The compiler tracks who owns what and who is borrowing what. If you try to use a borrowed value after the owner is gone, the compiler stops you. If you try to mutate data while someone else is holding a reference, the compiler stops you. These rules prevent dangling pointers and data races without needing a garbage collector.
Memory layout: what's actually happening
Owned types and borrowed types look different in memory. This difference explains why borrowing is cheap and why ownership matters.
fn main() {
// Owned: String allocates heap memory for the text.
// The variable `owned` holds a pointer, length, and capacity on the stack.
let owned = String::from("hello");
// Borrowed: &str points to the data inside `owned`.
// The variable `borrowed` holds a pointer and length on the stack.
// No heap allocation happens here.
let borrowed: &str = &owned;
println!("{owned}, {borrowed}");
}
A String is a struct containing three values on the stack: a pointer to the heap, the current length, and the allocated capacity. The actual characters live on the heap. When the String goes out of scope, Rust frees the heap memory.
A &str is a "fat pointer." It contains a pointer to the start of the text and the length. It has no capacity because you can't grow a borrowed view. It points to data owned by someone else. When the &str goes out of scope, nothing is freed. The data stays alive as long as the owner exists.
This layout makes borrowing incredibly efficient. Passing a &str to a function copies two machine words. Passing a String moves the pointer, which is also fast, but it transfers ownership. If you want to keep using the String after the call, you must clone it, which allocates new heap memory and copies all the bytes. That clone is expensive.
The cost of ownership
Ownership comes with a price. When you take ownership of a value, you become responsible for its lifecycle. This often means you can't use the original value anymore.
fn take_ownership(s: String) {
// s is owned here.
println!("I own: {}", s);
// s is dropped at the end of this function.
}
fn main() {
let name = String::from("Alice");
take_ownership(name);
// Error: E0382 (use of moved value).
// `name` was moved into `take_ownership`.
// println!("{}", name);
}
The compiler rejects the second print because name was moved. The String inside take_ownership owns the heap data now. If main could still use name, you'd have two owners trying to free the same memory. That's a double-free bug waiting to happen.
Borrowing avoids this cost. You can pass a reference without giving up the original value.
fn read_only(s: &str) {
// s is borrowed.
println!("I see: {}", s);
// No ownership transfer. `s` is just a view.
}
fn main() {
let name = String::from("Alice");
read_only(&name);
// OK: `name` is still owned by `main`.
println!("Still have: {}", name);
}
The convention in Rust is clear: borrow when you can, own when you must. Functions should accept borrowed types like &str or &[T] unless they genuinely need to take ownership. This lets callers pass owned data, borrowed data, or literals without cloning.
Borrowing with rules
Borrowing isn't free access. Rust imposes strict rules to keep data safe. You can have any number of immutable borrows (&T), or exactly one mutable borrow (&mut T), but never both at the same time.
fn main() {
let mut v = vec![1, 2, 3];
let ref1 = &v[0]; // Immutable borrow.
let ref2 = &v[1]; // Another immutable borrow. OK.
// v.push(4); // Error: E0502.
// Cannot borrow `v` as mutable because it is also borrowed as immutable.
println!("{ref1}, {ref2}");
// Mutable borrow requires exclusive access.
let ref_mut = &mut v[0];
*ref_mut = 99;
// println!("{}", ref1); // Error: E0502.
// Cannot use `ref1` while `ref_mut` is active.
}
The compiler rejects the mutable push while immutable references exist. This rule prevents data races and invalidates references caused by reallocation. If you push to a vector, the vector might reallocate its buffer. Any existing references to the old buffer would become dangling pointers. Rust forbids the mutation while references are alive, so this crash can never happen.
The error E0502 is one of the most common borrow checker errors. It tells you that you're trying to mutate data while someone else is looking at it. The fix is usually to narrow the scope of the borrow or restructure the code so the mutation happens before or after the read.
Realistic example: function signatures
The owned vs borrowed distinction shows up most often in function signatures. Choosing the right type affects flexibility and performance.
/// Returns the first word of the input text.
/// Accepts &str to work with String, &str, or literals.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
// Return a slice of the original data.
// No allocation needed.
return &s[..i];
}
}
&s[..]
}
fn main() {
let text = String::from("hello world");
// Auto-ref: &text converts String to &str.
let word = first_word(&text);
println!("Word: {}", word);
// Literal is &str.
let word2 = first_word("rust is fun");
println!("Word2: {}", word2);
}
The function first_word takes &str, not String. This is a critical design choice. If it took String, callers would have to clone their data or give up ownership. By taking &str, the function accepts any string-like input. The caller passes &text, and Rust automatically converts the String to a &str via the Deref trait. This is called "auto-ref." It makes the API flexible without runtime cost.
The return type is also &str. The function returns a slice of the input data. It doesn't allocate a new String. The returned reference borrows from the input. The caller must ensure the input lives long enough. If you return a reference, the caller inherits the borrow.
Convention aside: always prefer &str in function parameters unless you need to mutate the string or store it in a struct. The same rule applies to collections. Prefer &[T] over Vec<T> in arguments. It lets you accept vectors, arrays, and slices uniformly.
Pitfalls and compiler errors
New Rust developers hit a few predictable walls when learning owned vs borrowed types.
Dangling references are the most common. You try to return a reference to a local variable. The compiler rejects this with E0515 (cannot return reference to local variable). The local variable is dropped when the function returns. The reference would point to freed memory. The fix is to return an owned type or borrow from the caller's data.
fn bad_borrow() -> &str {
let s = String::from("hello");
// Error: E0515.
// `s` is dropped at the end of the function.
// &s would be dangling.
&s
}
Another pitfall is holding a borrow too long. You create a reference, use it, then try to mutate the data. The compiler complains with E0502. The borrow is still active even if you're not using the variable right now. Rust's borrow checker is static; it doesn't track data flow at runtime. The fix is to drop the borrow explicitly or use a block to limit its scope.
fn main() {
let mut v = vec![1, 2, 3];
{
let ref1 = &v[0];
println!("{}", ref1);
// ref1 is dropped here.
}
// OK: borrow is gone.
v.push(4);
}
The block { ... } creates a new scope. When the block ends, ref1 is dropped. The mutable borrow is allowed afterward. This pattern is common when you need to inspect data before mutating it.
Decision: when to use owned vs borrowed
Choosing between owned and borrowed types is a design decision. Use the right tool for the job.
Use String when you need to create new text or modify existing text in place. Use &str when you only need to read text or pass it to a function; almost every standard library function accepts &str, not String. Use Vec<T> when you need a dynamic list that grows or shrinks. Use &[T] when you need to inspect a sequence of items without taking ownership; this lets you work with slices of vectors, arrays, or even string bytes. Use owned types when the data must survive beyond the current function call or scope. Use borrowed types when you want zero-cost access to data that someone else manages.
Pass &str in your API. It's the polite thing to do. Borrowing is cheap. Ownership is a commitment.