Rust Interview Questions · 2026

Rust Interview Questions (2026): Most Asked, With Answers

A backend candidate interviewing for a systems role at a payments infrastructure startup last quarter got stuck on a question that had nothing to do with syntax: why does a function that takes a Vec<String> by value force the caller to give up ownership, while a function that takes &Vec<String> doesn't. He'd shipped a working Rust service for eight months and had solved the borrow checker's complaints by adding .clone() wherever the compiler turned red, never once stopping to ask what the checker was actually protecting him from. Rust has been voted the most admired language in the Stack Overflow Developer Survey for ten years running, and in the 2025 edition 72.8 percent of developers who use it want to keep using it, the highest retention of any language measured (Stack Overflow, 2025). Companies hiring for it, AWS, Cloudflare, Discord, Microsoft's Azure and Windows teams among them, know that admiration and interview-readiness are two different things, and they screen accordingly.

Here's the uncomfortable part: most Rust prep material spends its time on syntax that looks unfamiliar coming from another language, match expressions, the question mark operator, iterator chains, because those are easy to demo in a short code snippet. Ownership and borrowing is the thing that actually decides whether a candidate understands Rust or is translating Python or Java into Rust-shaped syntax. You can pass a phone-screen by memorizing that Box puts something on the heap. You can't survive a real code review, or a systems-design round that touches concurrency, without being able to explain why the compiler rejected a borrow, and what to change instead of reaching for .clone() or unsafe.

This page covers Rust interview questions across nine areas: ownership and borrowing, lifetimes, traits and generics, error handling with Result and Option, concurrency and Send/Sync, smart pointers, memory layout and unsafe code, pattern matching and enums, and the tooling questions that come up around cargo and modules. Every code sample is real Rust, checked for the syntax rules of a current stable compiler, not pseudocode.

49Questions
Ownership & BorrowingCore Concept
2025 Stack Overflow SurveyMost Admired Language
Rust CodeFormat

Ownership and borrowing: the question every loop opens with

This is where a Rust interview lives or dies in the first ten minutes. A candidate who can only recite "the borrow checker prevents data races" without being able to walk through why a specific snippet fails to compile hasn't actually internalized the model yet.

Easy questions

12

Every value has exactly one owner at a time. When the owner goes out of scope, the value is dropped. Ownership can be transferred, moved, to another variable, and once moved, the original variable can no longer be used. Those three rules are the entire foundation the rest of the type system builds on, no garbage collector, no manual free, just scope-based cleanup enforced at compile time.

rust
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2

println!("{}", s1); // compile error: s1 was moved

It comes down to whether the type implements the Copy trait. Simple stack-only types, integers, floats, booleans, and char, implement Copy, so assignment duplicates the bits and both variables stay valid. String owns a heap allocation, a pointer, length, and capacity on the stack pointing at data on the heap, so a bitwise copy would leave two owners pointing at the same heap memory. Rust refuses to let that happen implicitly, so it moves ownership instead of copying, which is why the original variable becomes invalid after assignment.

rust
let x = 5;
let y = x; // Copy, both x and y are valid, i32 is Copy

let s1 = String::from("hi");
let s2 = s1; // Move, s1 is no longer valid

A shared reference, &T, lets you read a value without taking ownership, and you can have any number of them active at the same time. A mutable reference, &mut T, lets you modify the value through the reference, but only one mutable reference can exist at a time, and it can't coexist with any shared reference to the same data. That rule is what actually rules out data races at compile time: you either have many readers or one writer, never both.

rust
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s; // fine, multiple shared references
println!("{r1}, {r2}");

let r3 = &mut s; // fine now, r1 and r2 are no longer used after this point
r3.push_str(" world");

A lifetime is the scope for which a reference stays valid. The compiler tracks lifetimes on every reference automatically, in the vast majority of code you never write the annotation because it can infer them. The explicit 'a syntax only becomes necessary when the compiler can't figure out the relationship between input and output lifetimes on its own, most often in a function signature that returns a reference derived from more than one input reference.

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

A struct defines the shape of data, its fields and their types. A trait defines behavior, a set of method signatures that a type can implement, similar to an interface in Java or a protocol in Swift. A single struct can implement any number of traits, and the same trait can be implemented by many unrelated structs, which is how Rust achieves polymorphism without inheritance.

rust
trait Summary {
  fn summarize(&self) -> String;
}

struct Article { title: String }

impl Summary for Article {
  fn summarize(&self) -> String {
    format!("Article: {}", self.title)
  }
}

Option<T> represents a value that might be absent, Some(T) or None, used for things like looking up a key that might not exist. Result<T, E> represents an operation that might fail with a specific error, Ok(T) or Err(E), used for things like reading a file or parsing a string, where you actually want to know why it failed, not just that it did.

rust
fn find_user(id: u32) -> Option<String> {
  if id == 1 { Some("Alice".to_string()) } else { None }
}

fn parse_age(input: &str) -> Result<u32, std::num::ParseIntError> {
  input.parse::<u32>()
}

Applied to a Result, ? unwraps the Ok value if the result was successful, or immediately returns from the enclosing function with the Err value if it wasn't, converting the error type through From along the way if the function's return error type differs from the one produced. It's syntactic sugar for a match statement that would otherwise take four or five lines at every single call site where an error might need to propagate.

rust
fn read_and_parse(path: &str) -> Result<u32, Box<dyn std::error::Error>> {
  let contents = std::fs::read_to_string(path)?; // returns early on error
  let n = contents.trim().parse::<u32>()?;
  Ok(n)
}

Because .unwrap() panics immediately if the value is Err or None, and a panic in a normal thread unwinds the stack and terminates that thread, which usually means the whole process if it happens on the main thread. It's fine in a test, a quick prototype, or a case you've already proven is unreachable, but shipping .unwrap() on a path that handles untrusted input, a network response, user-supplied data, a file that might not exist, turns a recoverable error into a crash. The idiomatic alternative is ?, or a match with an actual fallback.

rust
let n: u32 = "not a number".parse().unwrap(); // panics at runtime

let n: u32 = "not a number".parse().unwrap_or(0); // falls back instead

Box<T> puts T on the heap instead of the stack, while still giving you single, compile-time-checked ownership of it, same rules as any other value, just one layer of heap indirection. The two common reasons to reach for it: a recursive type whose size the compiler can't compute at compile time (a linked list node containing another node would be infinitely sized without boxing one side), and moving a large value cheaply, since moving a Box only copies the pointer, not the data it points to.

rust
enum List {
  Cons(i32, Box<List>), // Box breaks the infinite size problem
  Nil,
}

A Rust enum variant can carry its own associated data, of any shape, different variants can even carry entirely different data, not just an integer tag the way a C enum works. Option<T> and Result<T, E>, both baked into the standard library, are ordinary enums built on exactly this feature, Some(T) carries a value, None carries nothing.

rust
enum Shape {
  Circle { radius: f64 },
  Rectangle { width: f64, height: f64 },
  Triangle(f64, f64, f64),
}

The compiler refuses to compile a match that doesn't cover every possible variant, unless you add a catch-all arm with _ or a binding pattern, precisely so that adding a new variant to an enum later forces every match on it across the codebase to be revisited, rather than silently falling through to nothing and producing a bug that only shows up at runtime.

rust
enum Direction { North, South, East, West }

fn describe(d: Direction) -> &'static str {
  match d {
    Direction::North => "up",
    Direction::South => "down",
    Direction::East => "right",
    // error: missing match arm for West
  }
}

#[derive(...)] is a procedural macro that generates a trait implementation automatically at compile time, based on the shape of the struct or enum it's attached to, instead of you writing that implementation by hand. Debug, Clone, PartialEq, and Hash are the ones you'll see constantly, since implementing any of them correctly by hand for a struct with several fields is repetitive, mechanical work that a macro can generate correctly every time.

rust
#[derive(Debug, Clone, PartialEq)]
struct Point {
  x: i32,
  y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
assert_eq!(p1, p2); // needs PartialEq
println!("{:?}", p1); // needs Debug

Medium questions

23

Take a reference instead of ownership, since the function doesn't actually need to own the value, it just needs to read it. Passing &String (or, more idiomatically, &str so the function also accepts string slices and literals) means the caller keeps ownership the whole time and the function borrows temporarily.

rust
// awkward: takes ownership, has to hand it back
fn print_len(s: String) -> String {
  println!("{}", s.len());
  s
}

// idiomatic: borrows, caller keeps ownership
fn print_len(s: &str) {
  println!("{}", s.len());
}

This is one of the fastest tells in an interview. A candidate who reaches for the borrow immediately has internalized ownership. A candidate who reaches for .clone() to make the error go away usually hasn't.

Rust calls the value's Drop::drop implementation, if it has one, then recursively drops each of its fields. For a String, that means the heap buffer gets deallocated. For a struct holding a file handle or a network connection, Drop is where you'd close it. This happens deterministically, at the closing brace of the scope the value was declared in, not at some unpredictable point later the way a garbage collector would schedule it.

rust
struct Logger;

impl Drop for Logger {
  fn drop(&mut self) {
    println!("Logger dropped");
  }
}

fn main() {
  let _l = Logger;
  println!("doing work");
} // "Logger dropped" prints here, at end of scope

Because the local variable is dropped at the end of the function, and a reference to dropped memory would be dangling, pointing at memory that's no longer valid to read. Rust's borrow checker rejects this at compile time rather than letting it become a use-after-free bug at runtime, which is a class of memory-safety error that C and C++ programs run into constantly.

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

// fix: return the owned value instead
fn no_dangle() -> String {
  let s = String::from("hello");
  s // ownership moves out to the caller
}

By default, a closure borrows the variables it uses from the surrounding scope, taking the least restrictive kind of borrow each variable's usage needs, shared reference if it's only read, mutable reference if it's mutated. The move keyword forces the closure to take ownership of every captured variable instead, which is required whenever the closure needs to outlive the scope it was created in, most commonly when spawning a thread.

rust
let data = vec![1, 2, 3];

let handle = std::thread::spawn(move || {
  println!("{:?}", data); // data is moved into the thread
});

handle.join().unwrap();
// data is no longer usable here, it was moved

It's promising that the returned reference will be valid for at least as long as the shorter of the two input lifetimes, since either x or y could be returned and the caller has no way to know which one at compile time. The annotation doesn't change how long anything actually lives, it just describes a relationship that already exists so the borrow checker can verify the caller's usage is safe.

rust
let s1 = String::from("long string");
let result;
{
  let s2 = String::from("short");
  result = longest(s1.as_str(), s2.as_str());
} // s2 dropped here
println!("{result}"); // error: result may borrow from s2, which is gone

First, each elided reference parameter gets its own distinct lifetime. Second, if there's exactly one input lifetime, it's assigned to every elided output lifetime. Third, if one of the parameters is &self or &mut self, the lifetime of self is assigned to every elided output lifetime. Those three rules cover the overwhelming majority of real function signatures, which is why most Rust code you read never has an explicit lifetime annotation at all.

rust
fn first_word(s: &str) -> &str { // rule 2 applies, no annotation needed
  s.split_whitespace().next().unwrap_or("")
}

'static means a reference is valid for the entire remaining duration of the program, it can be held as long as needed without ever becoming dangling. String literals are &'static str automatically, since they're compiled directly into the binary. It's a common misconception that 'static means "lives forever" in a stronger sense, it really just means "could live that long if you keep it around," a value with a 'static bound doesn't have to actually be held until the program exits, it just has to not contain any non-static references.

rust
let s: &'static str = "I live for the whole program";

A struct holding owned data, a String instead of a &str, is self-contained: it manages its own memory and has no lifetime parameter, at the cost of an extra allocation and copy if the data originated somewhere else. A struct holding a reference needs a lifetime parameter on the struct itself, and every instance of that struct can't outlive the data it borrows from. Interviewers use this to see whether you'd reach for owned data by default and only borrow when the allocation actually matters, since a struct full of lifetime parameters tends to spread that complexity through every function that touches it.

rust
struct Excerpt<'a> {
  part: &'a str,
}

let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { part: first_sentence }; // excerpt can't outlive novel

A generic function bounded by a trait, fn f<T: Summary>(item: T), gets monomorphized: the compiler generates a separate copy of the function for each concrete type used, so the call is resolved at compile time with zero runtime overhead, at the cost of larger binary size. dyn Trait uses a vtable, a table of function pointers built at runtime, so one function can accept any type implementing the trait through a single compiled code path, at the cost of a pointer indirection on every call and losing the ability to inline.

rust
fn notify_static<T: Summary>(item: &T) { println!("{}", item.summarize()); }

fn notify_dynamic(item: &dyn Summary) { println!("{}", item.summarize()); }

let items: Vec<Box<dyn Summary>> = vec![Box::new(Article { title: "Rust".into() })];

A trait bound restricts a generic type parameter to types that implement a given trait, so the compiler knows which methods are available inside the function body. Inline bounds get unreadable fast once you have more than one or two type parameters with multiple bounds each, so Rust lets you move them into a separate where clause after the parameter list, which reads closer to a sentence than a wall of angle brackets.

rust
fn process<T: Clone + std::fmt::Debug, U: Clone + std::fmt::Debug>(t: T, u: U) {}

// equivalent, easier to read with more bounds
fn process<T, U>(t: T, u: U)
where
  T: Clone + std::fmt::Debug,
  U: Clone + std::fmt::Debug,
{
}

panic! is for situations the program considers a bug, an invariant that should never be violated if the code is correct, an out-of-bounds array access, an assertion that fails. Returning Err is for failures that are expected as part of normal operation and that the caller might reasonably want to recover from, a network timeout, a file that doesn't exist, invalid user input. The rule of thumb from the Rust book: use Result whenever failure is expected and the caller has a meaningful way to respond, reserve panic! for cases where continuing would leave the program in a state that's actually unsafe or meaningless to keep running in (The Rust Programming Language, ch. 9).

The enclosing function's return type has to be Result<_, E> or Option<_>, matching the kind of value ? is being applied to, and the error type E has to support conversion via From from whatever error type the failing expression produces. You can't use ? inside fn main() unless main itself is declared to return a Result, which Rust does support as of a fairly old edition, precisely so small programs and examples can use ? at the top level.

rust
fn main() -> Result<(), std::io::Error> {
  let contents = std::fs::read_to_string("config.toml")?;
  println!("{contents}");
  Ok(())
}

Send means a type can be safely transferred to another thread. Sync means a type can be safely shared between threads via a reference, formally, T is Sync if &T is Send. They're marker traits, no methods, because they don't describe behavior to implement, they describe a property the compiler can verify automatically for almost every type based on its fields. Most types get both auto-implemented for free; the compiler only withholds them for types that genuinely aren't safe across threads, like Rc, which uses a non-atomic reference count that would corrupt under concurrent access.

Rc's reference count is a plain, non-atomic integer, incrementing and decrementing it from two threads at once is a data race that could corrupt the count and cause a double free or a leak. Arc, atomic reference count, uses atomic operations for the increment and decrement instead, which costs a little more overhead per clone but is safe across threads. If you also need to see the ownership rules enforced across threads, you'll almost always see Arc paired with Mutex or RwLock for the interior mutability, since Arc alone only gives you shared read access, not mutation.

rust
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(); }
println!("Result: {}", *counter.lock().unwrap()); // 10

An OS thread is scheduled by the operating system and has its own stack, typically a few megabytes reserved, which makes spawning thousands of them expensive. An async task is a state machine the Rust compiler generates from an async fn, scheduled cooperatively by a runtime like tokio on top of a small pool of OS threads, so spawning tens of thousands of tasks is realistic in a way spawning tens of thousands of OS threads isn't. The tradeoff: async tasks require every blocking operation in the call chain to be async-aware, calling a blocking, synchronous function inside an async task stalls the entire worker thread it's running on, taking every other task scheduled on that thread down with it.

rust
#[tokio::main]
async fn main() {
  let handle = tokio::spawn(async {
    println!("running on the tokio runtime");
  });
  handle.await.unwrap();
}

A channel lets one thread or task send values that another receives, without both sides needing to share a lock directly, the "share memory by communicating" style Rust's standard library and the concurrency community generally favor over raw shared state. The standard library's mpsc, multiple producer, single consumer, lets any number of senders push values but only one receiver drains them, each value delivered exactly once, to whichever call to recv gets it first. A broadcast channel, from tokio, delivers every sent value to every currently subscribed receiver, useful for something like a shutdown signal or an event feed multiple independent consumers all need to see.

rust
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
  tx.send("hello from the other thread").unwrap();
});

println!("{}", rx.recv().unwrap());

The same ownership and borrowing rules that stop two parts of the same program from mutating a value at the same time also apply across threads, so any type that isn't safe to share is simply not Send or Sync, and the compiler refuses to let you send or share it, turning what would be a runtime data race in most languages into a compile error before the program ever runs.

Rc<T>, reference counted, allows multiple owners of the same heap value, tracked at runtime by incrementing a count each time Rc::clone is called and decrementing it each time a clone is dropped, freeing the value only once the count hits zero. A plain reference doesn't own anything and is tied to a specific lifetime, borrowed and returned within a scope the compiler can verify statically. You reach for Rc specifically when a value genuinely needs more than one owner and you can't structure the code so ownership is single and clear, a node in a graph that multiple other nodes need to point at is the classic case.

rust
use std::rc::Rc;

let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // count is now 2, no deep copy of the string
println!("count: {}", Rc::strong_count(&a)); // 2

unsafe unlocks five specific abilities the compiler otherwise forbids: dereferencing a raw pointer, calling an unsafe function or method, accessing or modifying a mutable static variable, implementing an unsafe trait, and accessing a field of a union. It does not turn off the borrow checker for safe code around it, and it does not turn off type checking, integer overflow behavior in debug builds, or any other check unrelated to those five abilities. The point of unsafe is narrowly scoped: it's a promise to the compiler that you've manually verified an invariant it can't check itself, not a general escape hatch from the rest of the language's rules.

An array's size is fixed at compile time and part of its type, [i32; 5] is a distinct type from [i32; 10], and it lives entirely on the stack (or wherever it's embedded, inline in a struct, for instance) with no heap allocation. A Vec<T> is a small stack-resident struct, a pointer, a length, and a capacity, pointing at a heap-allocated buffer that can grow, reallocating and copying elements to a larger buffer whenever it runs out of capacity. That's the core tradeoff: arrays are fixed-size and allocation-free, vectors are resizable at the cost of an allocation and the possibility of reallocation on growth.

if let is sugar for a match that only cares about one pattern and wants to ignore every other case, without the exhaustiveness requirement a full match imposes. It reads better when you genuinely only have one branch of logic and want to fall through silently otherwise, but it trades away the compiler's guarantee that every case is handled, which matters more the more variants a type has or the more likely it is to gain new variants later.

rust
let config: Option<String> = Some("debug".to_string());

if let Some(value) = &config {
  println!("config: {value}");
}
// equivalent, longer, matches every case explicitly
match &config {
  Some(value) => println!("config: {value}"),
  None => {}
}

A match guard is an extra if condition attached to a match arm, evaluated only if the pattern itself matches, letting you combine structural matching with an arbitrary boolean condition that isn't expressible as a pattern on its own, comparing two bound variables against each other, for instance, which a pattern alone has no syntax for.

rust
let pair = (3, -3);

match pair {
  (x, y) if x + y == 0 => println!("sums to zero"),
  (x, _) if x % 2 == 0 => println!("first is even"),
  _ => println!("no special relationship"),
}

Cargo.toml declares your dependencies and their acceptable version ranges, what you're willing to use. Cargo.lock pins the exact resolved version of every dependency in the full tree, what actually got used the last time the build ran, guaranteeing every machine that builds from the same lockfile gets bit-for-bit identical dependency versions. Applications commit the lockfile so every deploy and every teammate's machine builds the exact same dependency graph. Libraries usually don't, because a library's consumers resolve their own dependency graph, and pinning a library's lockfile would fight against the whole point of the flexible version ranges in Cargo.toml.

Hard questions

14

Two-phase borrows split a mutable borrow into a reservation phase and an activation phase. The reservation happens when the borrow is created, but it only becomes an exclusive, conflicting borrow at the point it's actually used mutably. That gap matters for a pattern like vec.push(vec.len()), which looks like it should fail (a mutable borrow of vec for push, while also reading vec.len() as an argument), but compiles because the shared read of vec.len() happens before the mutable borrow is activated.

rust
let mut vec = vec![1, 2, 3];
vec.push(vec.len()); // compiles thanks to two-phase borrows

Most candidates have never heard the term but have relied on the behavior without knowing it. Bringing it up unprompted signals you've actually read about how the borrow checker evolved past its original, stricter NLL-era rules, rather than just accepting whatever compiles.

Because dyn Trait relies on a vtable of function pointers with a fixed layout, a trait has to be object safe to be usable that way, meaning none of its methods can return Self by value or take generic type parameters. Both of those would require the vtable to know the concrete type at the call site, which defeats the entire point of type erasure that dyn provides. Clone is the classic example, its clone method returns Self, so Clone itself can't be used as dyn Clone.

rust
trait Clone {
  fn clone(&self) -> Self; // returns Self, not object-safe
}

// this doesn't compile:
// let items: Vec<Box<dyn Clone>> = vec![];

An associated type is a placeholder type defined inside a trait, filled in once per implementation, rather than a parameter the caller chooses at each call site. Iterator is the canonical example: it has one associated type, Item, because a given iterator only ever produces one kind of item, there's no reason a caller should be able to choose a different Item type for the same concrete iterator. A generic parameter on the trait itself, by contrast, would let a single type implement the trait multiple times with different type arguments, which is sometimes exactly what you want, and sometimes exactly what you want to prevent.

rust
trait Iterator {
  type Item;
  fn next(&mut self) -> Option<Self::Item>;
}

struct Counter { count: u32 }

impl Iterator for Counter {
  type Item = u32; // fixed once, for this type
  fn next(&mut self) -> Option<u32> {
    self.count += 1;
    if self.count < 6 { Some(self.count) } else { None }
  }
}

-> impl Trait returns a concrete, compiler-known type that implements the given trait, without naming that type, which is useful for returning closures or iterator chains whose real type is unnameable or painfully long. It's zero-cost, since the compiler still knows the exact type at compile time and can inline through it, unlike dyn Trait, which erases the type behind a vtable. The catch: every code path in the function has to return the exact same concrete type, so you can't use impl Trait to conditionally return two different types that both implement the same trait. That's the case where Box<dyn Trait> becomes necessary, since boxing erases the concrete type and lets both branches unify.

rust
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
  move |y| x + y
}

// two possible concrete types per branch, impl Trait can't unify them
fn make_op(subtract: bool) -> Box<dyn Fn(i32, i32) -> i32> {
  if subtract { Box::new(|a, b| a - b) } else { Box::new(|a, b| a + b) }
}

The mutex becomes poisoned. Rust's standard library Mutex tracks whether the last thread to hold the lock panicked, and if it did, every subsequent call to .lock() returns an Err wrapping a PoisonError instead of quietly handing back the guard, since the data protected by the lock might have been left in an inconsistent state mid-mutation when the panic happened. You can still recover the guard by calling .into_inner() on the error if you've verified the invariant wasn't actually broken, but the default behavior is to force you to think about it rather than silently continuing with possibly corrupted state.

A Future is a trait representing a computation that may not be finished yet, with a single required method, poll, that a runtime calls repeatedly to drive it forward until it returns Poll::Ready. Futures in Rust are lazy: writing async fn just builds a state machine implementing Future, and nothing inside it actually runs until something polls it, which in practice means awaiting it or handing it to a runtime's spawn function. This is different from JavaScript's promises, which start running the moment they're created, and it's a frequent source of a specific bug: constructing a future and never awaiting or spawning it, silently doing nothing.

rust
async fn do_work() {
  println!("this only prints if the future is awaited or spawned");
}

fn main() {
  let _future = do_work(); // nothing prints, the future was never polled
}

Interior mutability lets you mutate data through a reference that's itself immutable, by moving the borrow-checking from compile time to runtime. RefCell<T> enforces the same "one mutable borrow, or many shared borrows, never both" rule as normal references, but it checks that rule when .borrow() or .borrow_mut() is actually called, panicking if the rule is violated, instead of the compiler rejecting the code up front. That matters for something like a mock object in a test, or a node in a tree structure held behind an Rc, where the compiler's static rules are too conservative to allow a pattern you can prove is actually safe by construction.

rust
use std::cell::RefCell;

let cell = RefCell::new(5);
*cell.borrow_mut() += 1;
println!("{}", cell.borrow()); // 6

let _b1 = cell.borrow_mut();
let _b2 = cell.borrow_mut(); // panics at runtime: already mutably borrowed

Rc alone only gives you shared ownership of an immutable value, it doesn't let you mutate the thing it points to, since multiple owners mutating the same data outside a lock would violate exactly the rule the borrow checker exists to enforce. Wrapping the inner value in RefCell adds interior mutability on top, so Rc<RefCell<T>> gives you multiple owners that can each mutate the shared value, with the borrow rule enforced at runtime instead of compile time. It's the standard way to build things like a doubly-linked structure or a shared mutable cache in single-threaded code; the moment you need the same pattern across threads, you'd reach for Arc<Mutex<T>> instead, since neither Rc nor RefCell is thread-safe.

Yes, a reference cycle, two or more Rc values that point back at each other, means the strong count for each never reaches zero even though nothing outside the cycle can reach them anymore, so the memory leaks for the life of the program. Rust's ownership model prevents most memory bugs at compile time, but a leaked reference cycle isn't one of them, since from the type system's point of view every reference is still validly counted. The standard fix is Weak<T>, a non-owning reference that doesn't increment the strong count, used for the "back edge" of a cycle, a child pointing back at its parent, for instance, so the parent's strong count can still reach zero when nothing else references it.

rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
  parent: RefCell<Weak<Node>>, // doesn't keep the parent alive
  children: RefCell<Vec<Rc<Node>>>,
}

Cow, clone-on-write, is an enum that's either a borrowed reference or an owned value, and it only clones the data into an owned String the first time something actually needs to mutate it. That's useful for a function that usually just returns its input unchanged but occasionally needs to modify it, escaping a few special characters in a string, say, since it avoids an allocation on the common path where no modification is needed, while still working correctly on the rarer path that does need one.

rust
use std::borrow::Cow;

fn escape(input: &str) -> Cow<str> {
  if input.contains('&') {
    Cow::Owned(input.replace('&', "&")) // allocates, only when needed
  } else {
    Cow::Borrowed(input) // no allocation
  }
}

Because unsafe fn is also how a library author documents an invariant the caller has to uphold that the compiler has no way to verify, not just a signal that the function's body does something dangerous internally. Vec::get_unchecked is the classic example: its own implementation is memory-safe as long as the index is in bounds, but it skips the bounds check entirely for performance, so the function is marked unsafe to force the caller to explicitly promise, by writing the word unsafe at the call site, that they've already verified the index is valid some other way.

rust
let v = vec![1, 2, 3];
unsafe {
  let x = v.get_unchecked(10); // caller's responsibility: index must be valid
}

A panic is a controlled, defined response to an error, Rust guarantees the stack unwinds (or the process aborts, depending on configuration) in a predictable way. Undefined behavior means the language's rules have been violated in a way the compiler was trusting the programmer, not verifying itself, usually inside an unsafe block, dereferencing a dangling raw pointer, creating two mutable references to the same memory through raw pointers, reading uninitialized memory. Once undefined behavior happens, the compiler is no longer obligated to produce any predictable result at all; it might crash, it might silently corrupt unrelated memory, or it might happen to work by accident on one compiler version and break on the next. This is the sharpest distinction between safe and unsafe Rust: safe Rust's worst case is a panic, unsafe Rust done wrong has no worst case ceiling.

Overflow checks cost a branch on essentially every arithmetic operation, which is a meaningful performance cost in a release build compiled for production, so Rust defaults to wrapping (two's complement wraparound, the same behavior C has) in release mode for speed, while keeping the checks on and panicking on overflow in debug mode, where correctness during development matters more than raw throughput. This split is a real, documented source of "works in debug, breaks in release" bugs, since a wrapped value that silently becomes wrong is a much harder failure to notice than a panic that stops execution immediately. Teams that genuinely need overflow safety in release builds either enable the overflow-checks profile setting explicitly or use the explicit checked_add, wrapping_add, and saturating_add family of methods to make the intended behavior visible in the code itself.

rust
let x: u8 = 255;
let y = x.checked_add(1);  // None, overflow detected
let z = x.wrapping_add(1); // 0, wraps around
let w = x.saturating_add(1); // 255, clamps at the max

An enum with one variant per underlying error source, each wrapping the original error so no information gets thrown away, plus a From implementation per variant so ? can convert automatically. Implementing std::error::Error and Display on top makes the type usable anywhere a standard error is expected, which matters if the library is meant to compose with other error-handling code rather than force every caller to match on your enum by hand.

rust
#[derive(Debug)]
enum AppError {
  Io(std::io::Error),
  Parse(std::num::ParseIntError),
}

impl std::fmt::Display for AppError {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
    match self {
      AppError::Io(e) => write!(f, "io error: {e}"),
      AppError::Parse(e) => write!(f, "parse error: {e}"),
    }
  }
}

impl From<std::io::Error> for AppError {
  fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
  fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

In practice most teams reach for the thiserror crate to generate exactly this boilerplate from derive macros, and anyhow for application code that just needs to propagate errors up to a top-level log line without a typed enum. Knowing when to write it by hand versus reach for one of those two crates is itself a fair thing to be asked about.

How to prepare for a Rust interview in 2026

Don't start by memorizing syntax. Write one small thing that forces the borrow checker to actually fight you, a linked list, a small graph with parent and child pointers, a cache shared across a couple of threads, and then fix every error it raises by restructuring the ownership instead of reaching for .clone() or unsafe. That single exercise teaches ownership faster than any amount of reading about it, because you feel the constraint the compiler is actually enforcing instead of reading someone else's explanation of it.

Across systems and backend mock interviews run through LastRoundAI, the lifetime-elision question and the Rc<RefCell<T>> question trip up candidates more often than async/await does, even though async gets more attention in prep material. Our read is that most candidates study async because it feels like the "advanced" Rust topic worth rehearsing on purpose, while lifetimes and interior mutability get treated as things you'll just pick up by writing enough code. We don't have a precise figure for that gap from our own session data, it shows up often enough in review to be worth flagging here, not often enough that we'd attach a specific percentage to it.

One current detail worth knowing walking into 2026: Rust's official guidance on error handling still centers on the same distinction it's held for years, use Result when failure is expected and recoverable, reserve panic! for genuine bugs (The Rust Programming Language, ch. 9). What's shifted is tooling around it, thiserror and anyhow are close to a default choice on new projects now rather than a matter of individual taste, and an interviewer who's kept current will expect you to at least know when each one is the right call, even if you haven't used either on the job yet.

Get the reps in before the real thing

Explaining why a borrow fails on a whiteboard is not the same as fixing it live while an interviewer watches the compiler output scroll by. LastRoundAI's mock interview mode runs live coding rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $29/mo (or $19/mo on the 3-month plan) if a handful of sessions isn't enough runway before the interview you're actually prepping for.

Once your answers hold up under a follow-up question, the slower part of the job hunt is usually just getting in front of enough systems and backend roles that actually test Rust instead of listing it as a nice-to-have. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

Should I memorise Rust syntax for the interview?

Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.

What is the most common mistake in Rust interviews?

Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

How long does it take to prepare for a Rust interview?

If you already work with Rust day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What Rust topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Leave a Reply

Your email address will not be published. Required fields are marked *