← Back to blog

Rust interview questions

Rust interview questions and answers — ownership, borrowing, lifetimes and traits explained

There's a very specific stage of learning Rust where you stop fighting the syntax and start fighting the borrow checker personally. You write four lines of perfectly reasonable-looking code, hit compile, and get a wall of red text informing you that you've tried to use a value after moving it, except you don't remember moving anything, you just wanted to push a string into a vector. Forty minutes later you understand exactly what happened, and you also understand, on a cellular level, why every Rust developer's first instinct in an interview is to talk about ownership before anyone's even finished asking the question.

That instinct is correct. Rust interviews are unusually concentrated around a small number of core ideas — ownership, borrowing, lifetimes — because those ideas are genuinely what makes Rust different from every other systems language, and interviewers know that if you understand them deeply, the rest of the language (traits, error handling, concurrency) tends to follow naturally. This guide covers the Rust interview questions that actually come up, organized by area, with a real answer for each and a note on what it's actually testing.

Ownership and the borrow checker

Explain Rust's ownership model in your own words.

Every value in Rust has exactly one owner at a time. When the owner goes out of scope, Rust automatically calls drop and frees the value's memory — no garbage collector, no manual free(), and (this is the part interviewers care about) no possibility of a use-after-free or double-free bug, because the compiler enforces single ownership at compile time. Assigning a non-Copy value to a new variable, or passing it into a function, moves ownership rather than copying it — the original variable becomes invalid, and the compiler will refuse to let you use it again.

let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{}", s1); // compile error: s1 was moved

The interview signal here isn't reciting the rule — it's being able to explain why it exists: it's how Rust gets memory safety without a garbage collector's runtime cost.

What's the difference between borrowing and moving?

A move transfers ownership entirely — the original binding is invalidated. A borrow (&value or &mut value) lets you access a value temporarily without taking ownership, so the original owner is still valid afterward. The rule that trips up almost everyone learning Rust: you can have either any number of immutable borrows (&T) or exactly one mutable borrow (&mut T) active at the same time — never both. This rule alone eliminates an entire category of data races at compile time, because two threads can't simultaneously hold a mutable reference and an immutable one to the same data.

let mut v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v; // fine — multiple immutable borrows
// let r3 = &mut v; // compile error: can't borrow as mutable while borrowed as immutable
println!("{:?} {:?}", r1, r2);

Why doesn't Rust have a garbage collector, and what are the tradeoffs?

Ownership and borrowing let the compiler determine, at compile time, exactly when a value's memory can be freed — there's no need for a runtime garbage collector to track liveness, so there's no GC pause, no runtime memory-tracking overhead, and predictable performance characteristics similar to C/C++. The tradeoff is upfront cost, not runtime cost: you pay for memory safety with a steeper learning curve and the borrow checker's strictness, rather than paying for it with GC pauses or reference-counting overhead at runtime. This is the single most common "why Rust" interview question, and the honest answer is exactly this tradeoff — compile-time cost for runtime safety and speed.

What does it mean for Rust to have "zero-cost abstractions"?

High-level constructs (iterators, closures, generics, Option/Result) compile down to code that performs as well as the equivalent hand-written low-level code — you don't pay a runtime penalty for writing expressive, safe code. A classic example: chaining .iter().map(...).filter(...).sum() on a vector typically compiles to a tight loop with no intermediate allocations, thanks to iterator fusion at compile time, just as fast as a manual for loop you'd write in C.

What is the Copy trait, and why can't most types implement it?

Types that implement Copy (integers, floats, booleans, char, and tuples/arrays of Copy types) are duplicated on assignment instead of moved — both the original and the new binding remain valid. Most non-trivial types (String, Vec, anything managing heap memory or a resource like a file handle) can't implement Copy, because a bitwise copy would leave two owners pointing at the same heap allocation — exactly the double-free scenario ownership exists to prevent. This is why String is moved on assignment but i32 is copied: one manages a heap allocation, the other doesn't need to.

Lifetimes

What are lifetimes, and why does Rust need them?

A lifetime is the compiler's way of tracking how long a reference is guaranteed to remain valid, so it can reject any code where a reference might outlive the data it points to (a dangling reference) — a memory-safety bug that's extremely common in C/C++ and effectively impossible to compile in Rust. Most lifetimes are inferred automatically; you only need to annotate them explicitly ('a) when the compiler can't determine on its own how multiple references relate, typically in function signatures returning a reference derived from more than one input.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  if x.len() > y.len() { x } else { y }
}

Here 'a tells the compiler: the returned reference is valid for exactly as long as both x and y are valid — the function can't accidentally return a reference that outlives one of its inputs.

What is the "borrow checker," concretely — what is it actually checking?

The borrow checker is the part of the compiler that enforces two things simultaneously at every point in your code: that every reference is valid for its entire lifetime (no dangling references), and that the aliasing rules (many immutable borrows or one mutable borrow, never both) hold at every point. It does this entirely at compile time by analyzing the flow of ownership and borrows through your code — there's no runtime check, no performance cost, and no possibility of these bugs slipping through to production, which is the entire value proposition interviewers are checking you understand.

What is a dangling reference, and how does Rust prevent it at compile time?

A dangling reference points to memory that's already been freed — a classic source of crashes and security vulnerabilities in C/C++. Rust's borrow checker statically proves that any reference's lifetime never outlives the data it refers to; if you write code where that can't be proven, it simply won't compile.

fn dangle() -> &String {
  let s = String::from("hello");
  &s // s is dropped at the end of this function — compile error
}

The fix is to return the owned String itself (transferring ownership out) rather than a reference to a local that's about to be destroyed.

Explain lifetime elision — why don't I have to annotate lifetimes everywhere?

The compiler applies a small set of inference rules (lifetime elision rules) to handle the overwhelming majority of common function signatures without requiring explicit annotation — for example, a function taking one reference parameter and returning a reference is assumed to return something tied to that one input's lifetime. You only write explicit 'a annotations when a signature is ambiguous enough that the compiler's rules can't resolve it on their own, which in practice is a small minority of functions you'll write.

Diagram of Rust's ownership, borrowing and lifetimes rules and what they guarantee
Three rules, one payoff: memory safety with no garbage collector.

Traits and generics

What is a trait, and how does it differ from an interface in other languages?

A trait defines a set of methods a type can implement, similar in spirit to an interface in Java or Go. The key Rust-specific difference: traits can provide default method implementations that implementing types inherit for free, and — unlike interfaces in most OO languages — you can implement a trait for a type you don't own (as long as either the trait or the type is defined in your own crate, the "orphan rule"), enabling Rust's idiomatic extension-method style without modifying the original type's source.

trait Describable {
  fn describe(&self) -> String {
    String::from("an object")
  }
}
struct Dog;
impl Describable for Dog {
  fn describe(&self) -> String {
    String::from("a dog")
  }
}

What's the difference between static and dynamic dispatch (impl Trait vs dyn Trait)?

Static dispatch (impl Trait as a parameter, or generics with trait bounds) is resolved at compile time — the compiler generates a separate specialized version of the function for each concrete type used (monomorphization), so there's zero runtime overhead but a larger compiled binary. Dynamic dispatch (dyn Trait, accessed through a reference or Box) resolves which implementation to call at runtime via a vtable, costing a small indirection but letting you store different concrete types behind a single trait object in, say, one Vec<Box<dyn Trait>>. The interview answer interviewers want: use generics/impl Trait by default for performance; reach for dyn Trait when you genuinely need runtime polymorphism — a collection of heterogeneous types behind one interface.

What are trait bounds, and why use where clauses?

A trait bound restricts a generic type parameter to only types that implement a given trait, letting you call that trait's methods inside the generic function while still working for any qualifying type:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
  let mut largest = list[0];
  for &item in list {
    if item > largest { largest = item; }
  }
  largest
}

where clauses are the same constraint written differently — useful when bounds get long or involve multiple type parameters, since fn largest<T>(list: &[T]) -> T where T: PartialOrd + Copy reads more clearly than cramming everything into the angle brackets.

What is the difference between Option<T> and Result<T, E>, and why does Rust use them instead of null?

Option<T> represents a value that might be absent (Some(value) or None) — used for things like "this key might not be in the map." Result<T, E> represents an operation that might fail, carrying either success (Ok(value)) or an error (Err(error)) — used for fallible operations like file I/O or parsing. Both are enums the compiler forces you to handle explicitly (via match, if let, or the ? operator) before you can get at the inner value — there's no null in Rust, so the entire class of null-pointer-dereference bugs that plagues C, Java, and many other languages simply can't happen, because the type system won't let you forget to check.

Error handling

Explain the ? operator.

The ? operator, used on a Result or Option inside a function that returns the matching type, unwraps the success value and continues execution — or, if the value is an error/None, immediately returns that error/None from the enclosing function. It's syntactic sugar that replaces verbose match-and-early-return boilerplate:

fn read_username() -> Result<String, std::io::Error> {
  let mut s = std::fs::read_to_string("username.txt")?;
  s.truncate(s.trim_end().len());
  Ok(s)
}

Without ?, that line would be a full match statement returning early on Err. Interviewers ask this because it's the idiomatic way real Rust code handles errors — reaching for .unwrap() everywhere instead is a strong signal of inexperience.

When should you use .unwrap(), .expect(), or proper error propagation?

.unwrap() and .expect() panic immediately if the value is None/Err — appropriate in tests, quick prototypes, or genuinely unreachable cases you can prove won't happen (and .expect("reason") at least documents why you believe that). In production code paths that can plausibly fail — user input, file I/O, network calls — propagate the error with ? or handle it explicitly, so a single bad input doesn't crash the whole program. The interview-grade answer: panicking is for programmer errors and invariant violations; Result propagation is for expected, recoverable failure modes.

What is the difference between panic! and returning a Result?

panic! unwinds (or aborts) the current thread immediately — appropriate for unrecoverable programmer errors (an out-of-bounds index, a broken invariant) where continuing would be worse than stopping. Returning a Result lets the caller decide how to handle a failure that's an expected part of normal operation (a missing file, invalid user input, a failed network request) — the function signals the possibility of failure in its type, and the caller chooses to retry, fall back, or propagate further up. A library crate should almost never panic on bad input; an application binary handling a truly unrecoverable state may reasonably choose to.

Concurrency

What does "fearless concurrency" mean in Rust?

Rust's ownership and borrowing rules — the same rules that prevent dangling references and double-frees — also prevent data races at compile time, because two threads can't simultaneously hold a mutable reference to the same data, and the Send/Sync marker traits ensure only types safe to move between threads or share across threads are allowed to. The practical result: a huge class of concurrency bugs (use of unsynchronized shared mutable state) simply fail to compile, rather than surfacing as an intermittent, hard-to-reproduce production bug — which is the property "fearless" refers to.

What's the difference between Send and Sync?

Send means a type is safe to transfer ownership of across thread boundaries. Sync means a type is safe to share a reference to across threads — concretely, T is Sync if &T is Send. Most types are both automatically; Rc<T> is neither (its reference count isn't thread-safe), while its thread-safe counterpart Arc<T> is both. Interviewers ask this to check whether you understand that Rust's concurrency safety isn't magic — it's two specific marker traits the compiler checks, and the standard library types are designed around them deliberately.

How do Mutex<T> and Arc<T> work together for shared mutable state across threads?

Arc<T> (atomically reference-counted pointer) lets multiple threads share ownership of the same heap-allocated value, incrementing/decrementing a thread-safe reference count as clones are created and dropped. Mutex<T> wraps a value and only allows access through a lock that grants exclusive, mutable access while held — combined as Arc<Mutex<T>>, multiple threads can each hold a clone of the Arc, and each must acquire the Mutex's lock before touching the inner value, with the compiler enforcing that you can't even attempt to read the value without locking first.

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
  let counter = Arc::clone(&counter);
  handles.push(thread::spawn(move || {
    let mut num = counter.lock().unwrap();
    *num += 1;
  }));
}
for handle in handles { handle.join().unwrap(); }

What's the difference between std::thread and async/await in Rust?

std::thread::spawn creates a genuine OS thread — straightforward, but each thread carries real OS-level overhead, so it doesn't scale to thousands of concurrent tasks. async/await compiles to a state machine driven by an executor (Tokio is the dominant one) that multiplexes many concurrent tasks onto a small number of OS threads, making it the right choice for I/O-bound workloads with high concurrency (web servers, network clients) where most tasks spend their time waiting, not computing. The interview-grade distinction: threads for CPU-bound parallel work, async for I/O-bound concurrent work — and Rust, unusually, gives you precise control over both rather than picking one model for you.

Smart pointers and collections

Explain Box<T>, Rc<T>, and RefCell<T> — when do you reach for each?

Box<T> heap-allocates a value with single ownership — used when you need a value's size to be known at compile time but its content is recursive or large (a node in a linked list or tree), or when you want to store a dyn Trait object. Rc<T> enables multiple owners of the same heap-allocated value via reference counting, for single-threaded scenarios where ownership genuinely needs to be shared (a graph where multiple nodes reference a common child). RefCell<T> enables mutating a value even through an immutable reference, by moving Rust's borrow-checking from compile time to runtime — useful for the rare cases where the compiler's static rules are too conservative for a pattern you know is actually safe, at the cost of a runtime panic if the rules are violated instead of a compile error.

What is the difference between Vec<T>, arrays, and slices?

An array ([T; N]) has a fixed size known at compile time, stored inline (on the stack if it's a local variable). A Vec<T> is a growable, heap-allocated list that can change size at runtime. A slice (&[T]) is a view into a contiguous sequence of elements — borrowed, not owned — that works identically whether it's a view into an array, a Vec, or part of either; this is why most functions should accept &[T] rather than &Vec<T>, since a slice parameter accepts both.

The core truth: Rust interviewers aren't testing whether you can recite the borrow checker's rules — they're listening for whether you can explain why a specific snippet won't compile, and what the safe alternative is. That's the difference between having read the Rust book once and having actually fought the compiler enough times to internalize why it's right.

How Rust prep compares — and where most people get stuck

Most Rust interview prep falls into one of three buckets: a GeeksforGeeks-style question dump you read top to bottom the night before, the official Rust Book (genuinely excellent, but built for learning the language over weeks, not for rehearsing how to explain a concept out loud in five minutes), or pasting "explain Rust ownership" into ChatGPT and reading the answer silently. All three get you the content. None of them simulate the actual interview moment that trips people up most: an interviewer showing you a snippet that doesn't compile and asking you to explain why, live, with no time to look it up — which is a meaningfully different skill from being able to write correct Rust yourself when you have a compiler giving you instant feedback. The official Rust Book (doc.rust-lang.org) remains the right place to actually learn the language; the gap this guide and verbal practice fill is rehearsing the explanation under interview conditions.

That gap — between knowing something and being able to explain it clearly under a follow-up question you didn't expect — is exactly what verbal mock-interview practice targets, and it's also where Rust candidates specifically struggle, because the language's core concepts (ownership, lifetimes) are unusually hard to explain crisply even when you understand them well.

Practice explaining ownership out loud, not just writing it

You can write correct, borrow-checker-approved Rust and still freeze when an interviewer asks you to explain why a specific line won't compile, live, without a compiler to lean on. Greenroom runs spoken mock interviews that ask exactly these kinds of follow-ups on systems-language fundamentals, with feedback on how clearly you explain your reasoning — not just whether your final answer was technically correct. Pair this with our Go interview questions guide if you're comparing Rust against Go for a systems role, or our C++ interview questions guide if you're coming from a memory-management background and want the direct comparison to Rust's compile-time guarantees.

If you're prepping for a broader backend or systems-engineering interview loop, our system design interviews guide and backend developer interview questions guide cover what typically comes after the language-specific round.

Frequently asked questions

What are the most important Rust concepts to know for an interview?

Ownership, borrowing, and lifetimes are the core three — almost every Rust interview probes these directly or indirectly, since they're what makes Rust distinct from other systems languages. Beyond that: traits and generics (static vs dynamic dispatch), error handling with Option/Result and the ? operator, and concurrency basics (Send/Sync, Arc<Mutex<T>>).

Why does Rust not have a garbage collector?

Rust's ownership and borrowing rules let the compiler determine, at compile time, exactly when a value's memory can be freed, eliminating the need for a runtime garbage collector. This gives Rust C/C++-like performance with no GC pauses, at the cost of a steeper learning curve while the borrow checker's rules click into place.

What is the difference between Rc<T> and Arc<T>?

Both enable multiple owners of the same heap-allocated value via reference counting. Rc<T> is for single-threaded use only and has lower overhead since it doesn't need atomic operations. Arc<T> ("atomic Rc") uses atomic operations for its reference count, making it safe to share across multiple threads, at a small performance cost compared to Rc<T>.

Is Rust hard to learn for an interview if I come from Python or Java?

The syntax is approachable, but ownership and the borrow checker require a genuine mental model shift if you're coming from a garbage-collected language where you've never had to think about who owns a value or how long a reference is valid. Most candidates report the borrow checker "clicking" after a few weeks of writing real code and reading its error messages carefully — interviewers know this and tend to focus on whether you understand the concepts, not whether you've memorized every edge case.

Do Rust interviews ask about concurrency in detail?

It depends on the role — for backend/systems roles, yes, expect questions on Send/Sync, Arc<Mutex<T>>, and the difference between OS threads and async/await. For application-level or general-engineering roles where Rust is one of several languages used, concurrency questions are usually lighter, focusing on the high-level "fearless concurrency" pitch rather than implementation details.

What's the best way to practice explaining Rust concepts out loud?

Read the official Rust Book or docs to build the underlying understanding, then practice explaining specific concepts (ownership, a lifetime error, why a snippet won't compile) verbally to another person or in a mock interview — reading and reciting are different skills, and the gap between them is exactly what trips candidates up when a real interviewer asks a follow-up they didn't anticipate.

Rust interviews test whether you can explain why the borrow checker rejected something, not just recite the rules. Greenroom runs spoken mock interviews with realistic follow-ups and feedback on every answer. Free to start.
Try free →