Rust borrowing and lifetimes interview questions, with answers
Borrowing is how Rust lets code use a value without owning it, and lifetimes are how the compiler proves those borrows are safe. Together they are the part of Rust that takes longest to learn and the part interviewers most want you to explain in your own words: why two mutable references cannot exist at once, how the compiler rules out dangling references, and what a lifetime annotation actually says.
The answers below cover references, the borrowing rules, lifetimes and their elision rules, 'static and slices, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What are references and borrowing in Rust?
In short: A reference, &T or &mut T, lets code use a value without taking ownership; creating one is called borrowing, and the owner keeps the value afterwards.
Passing &v gives a function a reference to v instead of moving it, so main can still use v after the call, as below. A shared reference, &T, allows reading; a mutable reference, &mut T, allows changing the value, and requires the variable itself to be mut. References are always valid and never null: the compiler guarantees that the value outlives every reference to it. Functions that only need to look at data should take references, and slices such as &[i32] are the most flexible choice, because they accept a Vec, an array or part of either.
fn total(xs: &[i32]) -> i32 { xs.iter().sum() } fn main() { let v = vec![2, 3, 4]; println!("{}", total(&v)); // 9 println!("{}", v.len()); // 3 }
2.What are Rust's borrowing rules?
In short: At any time a value can have either any number of shared references or exactly one mutable reference, and every reference must be valid for as long as it is used.
The rules are short: many readers or one writer, never both, and no reference may outlive its value. Two live &mut references to the same variable are rejected, as in the code, because either could change the value while the other is relying on it; the same reasoning forbids a &mut while shared references are still in use. The checks rule out data races at compile time and also iterator invalidation, where a collection is modified while something still points into it. They apply per variable and are checked against where each reference is actually used, so a borrow that has ended no longer conflicts.
let mut s = String::new(); let a = &mut s; let b = &mut s; // does not compile a.push('x'); println!("{s}"); // x
3.How does Rust prevent dangling references?
In short: The borrow checker rejects any reference that could outlive the value it points to, such as a reference to a local variable returned from a function.
A dangling reference points to memory that has already been freed. In Rust, every reference has a lifetime, the region of code in which it is valid, and the compiler checks that no reference is used outside the lifetime of the value it borrows. A function that tries to return a reference to one of its own local variables fails to compile, because the local is dropped when the function returns; the fix is to return the owned value, as ok does below, and let the caller own it. The same check stops a reference from being stored in a longer-lived structure than its source.
fn ok() -> String { String::from("owned") } fn main() { println!("{}", ok()); // owned }
4.What are lifetime annotations in Rust?
In short: Annotations such as 'a name the relationships between references' lifetimes, so the compiler can check that, for example, a struct never outlives the data it borrows.
Lifetime annotations do not change how long anything lives. They describe constraints that the compiler then verifies. A struct that holds a reference must declare a lifetime parameter, as Word<'a> below, which says a Word cannot outlive the text it points to; the compiler then rejects any code where the String is dropped while a Word still refers to it. In function signatures, annotations say which inputs a returned reference may borrow from. Most functions need none, thanks to the elision rules, and when the compiler cannot infer the relationship, it asks for one.
struct Word<'a> { t: &'a str, } fn main() { let s = "hey".to_string(); let part = &s[..2]; let w = Word { t: part }; println!("{}", w.t); // he }
5.What are the lifetime elision rules in Rust?
In short: Three rules let the compiler infer lifetimes in common signatures: each input reference gets its own lifetime, a single input lifetime goes to all outputs, and &self's lifetime goes to the outputs of a method.
Writing 'a on every function would be tedious, so the compiler fills lifetimes in using three rules. First, each reference parameter gets a distinct lifetime. Second, if there is exactly one input lifetime, every output reference gets it, which is why head below needs no annotation: its result borrows from s. Third, in a method with &self or &mut self, output references get self's lifetime. If the rules do not settle every output lifetime, such as a function taking two references and returning one, the compiler asks you to write the annotation yourself, because it will not guess which input the result borrows from.
fn head(s: &str) -> &str { &s[..1] } fn main() { let h = head("rust"); println!("{h}"); // r }
6.What does 'static mean in Rust?
In short: &'static T is a reference valid for the whole program, like a string literal; a T: 'static bound means T holds no references shorter than that, not that it lives forever.
The 'static lifetime is the longest one: data that exists for the entire run of the program. String literals are &'static str because their text is stored in the compiled binary, which is why label below can return one without borrowing from any argument. The bound T: 'static, required for example by thread::spawn, is often misread: it means the type contains no borrowed references that could expire, so owned types such as String and Vec<i32> satisfy it even though their values are dropped normally. Leaking memory with Box::leak also produces a 'static reference, but that is rarely the right fix for a lifetime error.
fn label() -> &'static str { "fixed" } fn main() { let l = label(); println!("{l}"); // fixed }
7.What are slices in Rust?
In short: A slice, &[T] or &str, is a borrowed view of a contiguous run of elements: a pointer and a length into memory owned by something else.
A slice refers to part or all of an array, a Vec or a String without copying it. &a[1..4] below borrows three elements of the array, and its type, &[i32], says nothing about the length, which is stored alongside the pointer. Ranges exclude their end, so 1..4 covers indexes 1, 2 and 3, while 1..=3 includes it. Taking a slice is checked: an out-of-range end panics. Because slices are borrows, the borrowing rules apply: the collection cannot be changed while a slice of it is in use. Accepting &[T] and &str in function parameters makes code work with every owning type.
let a = [1, 2, 3, 4, 5]; let mid: &[i32] = &a[1..4]; println!("{:?}", mid); // [2, 3, 4]
How the diagnostic asks it
One question from the Rust bank, exactly as a sitting would show it. The bank has 3 on borrowing & lifetimes and 30 across Rust.
What does this Rust code print?
let mut s = String::from("hi"); let r = &s; println!("{}", r); s.push('!'); println!("{}", s);
- 1It does not compile: s is borrowed by r until the end of main
- 2hi, then hi!correct
- 3hi, then hi
- 4It does not compile: a String cannot be changed after it is borrowed
Since the 2018 edition, the borrow checker uses non-lexical lifetimes: a reference's borrow ends at its last use, not at the end of the block that declares it. r is last used in the first println, so by the time s.push('!') needs a mutable borrow, no shared borrow is active and the code compiles. It prints hi, then hi!. The first compile-error option describes the older, lexical rule. hi, then hi ignores the push. A String can be changed after being borrowed, as long as the borrow is over; using r again after the push would make the program fail to compile.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.