Skip to content
Languages

12 Rust Ownership and Lifetimes Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Ownership and Lifetimes material into 12 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

13 min read12 detailed answersReviewed Aug 24, 2026
What to remember

Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.

Question set

12 detailed answers

01

How does Rust guarantee memory safety without a garbage collector?

Short answer: Rust checks ownership and borrows statically, at compile time. The borrow checker proves every reference lives no longer than its data and that there's never simultaneous aliasing plus mutation — so use-after-free, double-free and data races are ruled out before the program ever runs. No GC and no reference counting by default: you pay in compile time and learning curve.

In depth:

  1. Ownership — every value has exactly one owner; when it leaves scope the memory is freed deterministically (Drop), with no runtime pauses.
  2. Borrowing — the compiler enforces "either many &T or one &mut T", which is what kills data races statically.
  3. Lifetimes — the borrow checker matches the lifetimes of references and data, preventing a dangling reference from ever existing.
  4. Zero-cost abstractions — the compiler pays for safety; there are no checks in the binary.
Criterion Rust GC languages
When checked compile time runtime
Runtime cost zero GC pauses / refcount
Failure mode compile error leaks, latency, rare bugs

⚠️ Common mistake: answering "Rust counts references, like ARC in Swift" or "there's a GC under the hood". By default there's neither — Rc/Arc are an opt-in tool, not the memory-safety mechanism.

02

Name Rust's three ownership rules. What happens to a value when its owner goes out of scope?

Short answer: (1) Every value has an owner; (2) there is exactly one owner at a time; (3) when the owner goes out of scope, the value is dropped. On leaving scope Rust automatically runs Drop and frees the resources — deterministically and with no garbage collector.

In depth:

  1. One owner — assignment or passing into a function moves ownership; the old binding becomes unusable.
  2. Exactly one at a time — shared access goes through borrowing (&/&mut), not through a second owner.
  3. End of scope → drop — at the closing } the destructor runs in reverse order of construction; this is a purely static decision by the compiler, not runtime tracking.
fn main() {
    let a = String::from("hi"); // a owns the heap buffer
    {
        let b = String::from("bye");
        // ...
    } // <- b is dropped here
    let c = a;                  // move: a is no longer valid
} // <- c is dropped here (former a's buffer); a was already moved

⚠️ Common mistake: describing drop as "GC-like runtime tracking". The compiler knows the destructor's timing statically — from scope and move rules, not from a reference count.

03

What physically happens when a value is moved — is it a deep copy?

Short answer: No. A move is a shallow bitwise copy of the value's stack representation (for a Vec that's pointer + length + capacity, 24 bytes on x86-64) plus the compiler invalidating the old binding. The heap data doesn't move or get copied — only the right to own it changes hands.

In depth:

  1. Only the stack part is copied — three machine words (ptr, len, cap), not the buffer itself.
  2. The heap stays put — the same allocated buffer now belongs to the new binding.
  3. The old binding is invalidated — using it after the move is a compile error, so the buffer keeps exactly one owner (no double-free).
  let v = vec![1,2,3];        let w = v;  (move)

  stack: v [ptr|len=3|cap=3]  stack: v  ✗ (unusable)
            │                        w [ptr|len=3|cap=3]
            ▼                            │
  heap:  [1][2][3]  ◄─────────── same buffer ─┘

⚠️ Common mistake: thinking a move "copies the data" or is an expensive operation. It copies a fixed 24 bytes off the stack regardless of the Vec's size — the heap buffer is never touched.

04

What's the difference between Copy and Clone? Why can't Vec<T> be Copy?

Short answer: Copy is a marker trait: the value is duplicated by implicit bitwise copy, no code runs, and assignment semantics switch from move to copy. Clone is explicit duplication via .clone() that can be arbitrarily expensive. Vec<T> can't be Copy because it owns a heap buffer: a bitwise copy would give two owners of one allocation → double-free.

In depth:

  1. Copy = bitwise copy — only for types that live entirely on the stack (i32, bool, char, &T); assignment no longer invalidates the source.
  2. Clone = explicit logic — the clone method can allocate and deep-copy (for Vec, allocate a fresh buffer and copy the elements).
  3. Why not Vec — a bitwise copy would copy the pointer, not the data; two Vecs would point at one buffer and both would run Drop.
Copy Clone
Invocation implicit, on = explicit, .clone()
Cost bitwise, cheap arbitrary
What it does move → copy duplicates, heap too
For Vec<T> ✗ forbidden ✓ (deep copy)

⚠️ Common mistake: thinking Copy "duplicates data". It adds no copying — it just switches assignment semantics from move to bitwise copy for types where that's safe.

05

Why can't a type implement both Copy and Drop?

Short answer: They're mutually exclusive contracts, and the compiler forbids combining them with error E0184. Copy means "the value may be silently duplicated bitwise", while Drop means "the type has custom resource cleanup". Two bitwise copies would both run Drop over the same resource → double-free.

In depth:

  1. Copy implies no owned resource — the type is trivial enough that a duplicate is indistinguishable from the original.
  2. Drop implies unique ownership — the destructor must run exactly once for exactly one owner.
  3. The conflict — if the compiler allowed both, let b = a; would make a copy, and at scope exit Drop would run for both a and b.
#[derive(Clone)]
struct Handle(i32);

impl Copy for Handle {}   // error[E0184]: the trait `Copy`
impl Drop for Handle {    // cannot be implemented for this type;
    fn drop(&mut self) {} // the type has a destructor
}

⚠️ Common mistake: trying to slap #[derive(Copy)] onto a type with a manual Drop to "get rid of moves". You have to pick one: either the type is a plain value (Copy), or a resource owner with cleanup (Drop).

06

Why won't the compiler let you call value.drop() directly, and how do you free a resource early?

Short answer: Drop::drop takes &mut self, not self — the value stays alive after the call, and at scope end the destructor would run a second time (double-free). So the compiler forbids calling .drop() explicitly. To free a resource early you use std::mem::drop(value) — a plain function that takes the value by ownership and lets it drop.

In depth:

  1. The signature is the obstacle — if .drop() were allowed, the binding would stay valid and Rust would run Drop again at }.
  2. The idiom is mem::drop — it's literally pub fn drop<T>(_x: T) {}: it takes T by value (move), the body is empty, and at its } the object is destroyed exactly once.
  3. The effect — ownership moves into the function, the original binding is invalidated, and the resource is freed immediately.
let guard = lock.lock().unwrap();
// guard.drop();        // ✗ error[E0040]: explicit use of destructor method
drop(guard);            // ✓ releases the mutex right here
// ... further code without holding the lock

⚠️ Common mistake: calling value.drop() as a method. The right way is the free function drop(value) that takes ownership; the manual Drop::drop is not meant to be called directly at all.

07

State the borrowing rule: why any number of &T but only one &mut T at a time?

Short answer: The rule is "aliasing XOR mutability": at any moment either any number of shared &T references, or exactly one exclusive &mut T, but never both at once. This statically rules out data races and iterator invalidation, and it also lets LLVM treat &mut T as noalias and optimize aggressively.

In depth:

  1. Many &T — readers don't interfere with each other, so an unlimited number of shared references is safe.
  2. One &mut T — a writer must be certain no one else is reading or writing the same place concurrently; hence exclusivity.
  3. Why so strict — combine reading and writing and you get a data race, or a reference into a Vec buffer that was already reallocated.
  4. Optimization — the noalias guarantee frees the compiler up, which is why breaking the rule via unsafe is UB, not a style question.
Simultaneously Allowed?
many &T
one &mut T
&T and &mut T together
two &mut T

⚠️ Common mistake: thinking the rule is "you can't mutate data while it's read, purely for convenience". It's about correctness: simultaneous aliasing plus mutation is UB, and the compiler relies on its absence.

08

Why does the compiler forbid returning a reference to a local variable?

Short answer: A local variable is destroyed when the function returns — its stack frame unwinds and the memory is freed. A reference to it would dangle, pointing at garbage. The borrow checker proves that data must outlive the references to it, can't prove that here, and rejects the code (E0515: cannot return reference to local variable).

In depth:

  1. Drop timing — at the function's closing } the local is dropped; returning a reference to it means handing out a pointer to already-freed memory.
  2. What the compiler checks — the referent's lifetime must cover the reference's lifetime; for a local it ends before the reference could ever be used. Note that a signature with no input references and no explicit lifetime (fn bad() -> &String) fails even earlier, at elision — E0106 "missing lifetime specifier".
  3. How to fix — return the value by ownership (move it out), not a reference; the caller becomes the owner.
fn bad<'a>() -> &'a String {
    let s = String::from("hi");
    &s               // ✗ error[E0515]: cannot return reference to local variable `s`
}                    //   s is dropped here → the reference would dangle

fn good() -> String {
    let s = String::from("hi");
    s                // ✓ hand ownership out
}

⚠️ Common mistake: answering just "it won't compile". The interviewer wants the drop-timing reasoning: the local dies on frame exit, so the reference would be guaranteed to dangle.

09

What are lifetimes and how do the three elision rules work?

Short answer: A lifetime is the region of code over which a reference must stay valid; it's a label for the borrow checker, not a runtime duration. Elision is a set of three rules by which the compiler fills in lifetimes for typical signatures so you don't have to write them by hand.

In depth:

  1. Rule 1 — each elided input reference gets its own distinct lifetime (fn f(a: &X, b: &Y) → two different ones).
  2. Rule 2 — if there is exactly one input lifetime, it's assigned to all output references.
  3. Rule 3 — if &self/&mut self is among the arguments, its lifetime goes to all output references (the common case for methods).

If after applying the rules some output reference still has an undetermined lifetime, the compiler demands an explicit annotation.

Elided form What the compiler sees
fn f(s: &str) -> &str fn f<'a>(s: &'a str) -> &'a str
fn g(x: &i32, y: &i32) fn g<'a,'b>(x: &'a i32, y: &'b i32)
fn m(&self, o: &str) -> &str output takes &self's lifetime

⚠️ Common mistake: thinking lifetime annotations affect how long data lives. They neither extend nor shorten anything — they only describe existing lifetime relationships to the borrow checker.

10

What does 'static mean as a reference lifetime vs as a T: 'static bound — e.g. in thread::spawn?

Short answer: &'static T means the data lives for the whole program (string literals, say, are baked into the binary). T: 'static means the type contains no references shorter than 'static — i.e. "can live for any length of time", not "lives forever". thread::spawn requires F: Send + 'static because the thread may outlive the frame that spawned it.

In depth:

  1. 'static as a reference lifetime — a concrete guarantee: the referent is valid until the program ends. Example: let s: &'static str = "hi";.
  2. T: 'static as a bound — a constraint on the type: it holds no borrows that could go stale. Owning types (String, Vec<i32>) satisfy it automatically.
  3. Why spawn needs it — the closure is handed to a new thread that may keep running after the spawning function returns; so it must not hold references to that function's locals.
Form Meaning
&'static T "this data lives until the program ends"
T: 'static "T contains no references shorter than 'static" → may live any length

⚠️ Common mistake: conflating the two meanings and thinking T: 'static forces the value to live forever. It only forbids short-lived borrows inside T; a String itself is happily dropped when it leaves scope.

11

What are NLL (non-lexical lifetimes) and how did they change the borrow checker?

Short answer: NLL is a model where a borrow ends at the reference's last use, not at the closing } of the lexical block. It was stabilized in Rust 2018, and thanks to it code that "violates" the textbook rule by the letter now compiles fine.

In depth:

  1. Before NLL — a reference's lifetime stretched to the end of the lexical scope, even if nothing touched it after its last use.
  2. With NLL — the borrow checker computes the borrow region from the actual control flow and cuts it off right after the last access.
  3. The effect — you can take &x, use it, then take &mut x in the same block; patterns like conditional borrows and early returns stopped being falsely rejected.
let mut v = vec![1, 2, 3];
let first = &v[0];        // ─┐ borrow begins
println!("{first}");      // ─┘ last use → it ends right here (NLL)
v.push(4);                // ✓ OK: &v[0] is no longer active
// pre-NLL: ✗ E0502 — first "lived" to the end of the block

⚠️ Common mistake: insisting a snippet won't compile based on the old "borrow lasts to the end of the block" rule. With NLL the borrow ends at its last use — and such code is valid.

12

What is variance in Rust: why is &'a T covariant but &mut T invariant in T?

Short answer: Variance describes how subtyping of one type carries over to a compound type. &'a T is covariant in T: a reference to a "longer-lived/more specific" type can be substituted where a weaker reference is expected — reading from it is safe. &mut T is invariant in T: you can also write through it, and a write would let a short-lived value be smuggled into a long-lived slot, so subtyping is forbidden in both directions.

In depth:

  1. Covariance (&T) — read-only, nothing is placed inside; substituting &'long where &'short is needed is safe ('long: 'short).
  2. Invariance (&mut T) — bidirectional access. Allow covariance here and you could write a short reference through &mut &'short into a cell of type &'long, leaving a dangling reference exposed afterwards.
  3. Cell/RefCell — invariant for the same reason: interior mutability grants writes, so variance can't be narrowed.
Type Variance in T Why
&'a T covariant read-only — stronger→weaker substitution is safe
&'a mut T invariant writable — otherwise a short lifetime could be smuggled in
Cell<T>/RefCell<T> invariant interior mutability = writes

⚠️ Common mistake: assuming &mut T is covariant "because a reference to a subtype is still a reference". Covariance breaks precisely because of writes: it would let short-lived data be stuffed into a long-lived cell and produce UB.

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Languages103 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers
RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS