Rust interview questions

Reviewed by Mark Dickie · Last updated

Rust is a systems programming language that guarantees memory safety and thread safety without a garbage collector, using a borrow checker to enforce ownership rules at compile time. For interviews, you should be solid on ownership, borrowing, and lifetimes — these are the concepts that distinguish Rust from every other language an interviewer might ask about. You should also understand traits and generics, error handling with Result and Option, and how Rust's concurrency model maps to Send/Sync and Mutex/Arc. Beyond syntax, interviewers often probe whether you can explain why the borrow checker rejects a given snippet and how to restructure the code to satisfy it.

ConceptWhat an interviewer checksCommon question shape
Ownership & movesCan you trace which variable owns a value after a move?"What prints here, and why?" with a code snippet
Borrowing & referencesDo you know when mutable and immutable borrows conflict?Spot-the-compile-error problems
LifetimesCan you annotate function signatures so references outlive their use?Fill in the lifetime parameters
Traits & genericsCan you write a trait bound and implement a trait for a type?Implement Iterator or Display for a custom type
Error handlingDo you reach for Result/? instead of panicking?Refactor unwrap calls into propagated errors
ConcurrencyCan you share state across threads safely?Why does this fail to compile, and how do you fix it?

What does a Rust interview typically test?

Most Rust interviews break down into a few recurring areas. Expect to encounter:

  1. Ownership and move semantics — predicting whether a value is copied or moved, and explaining the difference between Copy types and non-Copy types.
  2. Borrowing rules — the "one mutable OR many immutable" rule, and how to reason about it when multiple references are in scope.
  3. Lifetimes — writing correct lifetime annotations on functions and structs, and understanding elision rules.
  4. Traits and trait bounds — defining traits, implementing them, and using bounds like where T: Ord + Clone.
  5. Error handling — the Result/Option enums, the ? operator, and converting between error types.
  6. Concurrency primitivesArc, Mutex, RwLock, channel, and the Send/Sync marker traits.
  7. Smart pointers — when to use Box, Rc, RefCell, or their thread-safe variants, and what each costs at runtime.

How should I prepare for a Rust interview if I mostly write another language?

If you are coming from Python, JavaScript, or Go, the biggest adjustment is thinking in terms of ownership rather than garbage collection. Write small programs that the borrow checker rejects, then fix them — that muscle memory is what interviewers are actually testing. Read through the standard library docs for Option, Result, Vec, HashMap, and the Iterator trait; these types show up in almost every coding question. Practice implementing a trait from scratch (like Iterator for a custom collection) because that exercise covers generics, associated types, and lifetimes all at once.

Key facts

  • Tarmac has 89 Rust interview questions on this topic, 10 of them on this page, at difficulty 2–4 of 5.
  • Tarmac last reviewed these Rust interview questions on 23 August 2026.

At a glance

Questions10 shown · 89 in the bank
Difficulty2–4 of 5
FormatsMultiple choice, Fill in the blank, True / false, Code output, Short answer, Flashcard, Ordering, Multiple answer, Find the bug

What you'll review

  1. move semantics
  2. structs enums
  3. question operator
  4. iterators
  5. pattern matching
  6. panic unwrap
  7. box
  8. drop raii
  9. send sync
  10. borrow checker

Practice questions

Rust/ownership/move-semantics

You write let a = String::from("hi"); let b = a; println!("{}", a);. What does the compiler do?#

Options

Show answer

Reading a after let b = a is a compile error. String owns a heap buffer and is not Copy, so let b = a moves ownership to b and invalidates a; reading a afterward is a use-after-move that the borrow checker rejects before runtime ("borrow of moved value"). Copy types like i32 would duplicate instead. To keep both, use a.clone() or borrow with &a.

Why:

String owns a heap allocation and does not implement Copy, so let b = a is a move: ownership transfers to b and a is invalidated at compile time. Using a afterward is a use-after-move, and the borrow checker rejects it before the program ever runs — there is no runtime garbage or panic, just a compile error (borrow of moved value: a). Contrast a Copy type like i32, where let b = a duplicates the bits and both stay usable. To keep both String handles valid you must a.clone() (a deep copy) or borrow with &a. This compile-time move tracking is what lets Rust free each allocation exactly once without a garbage collector.

Rust/types-traits/structs-enums

To let the compiler auto-generate trait implementations for a struct, you annotate it with #[_____(Clone, Debug)]. With that in place, Debug enables printing the value using the _____ format specifier inside println!.#

Show answer

To let the compiler auto-generate trait implementations for a struct, you annotate it with #[**derive**(Clone, Debug)]. With that in place, Debug enables printing the value using the **{:?}** format specifier inside println!.

Why:

#[derive(...)] is the attribute that asks the compiler to auto-implement common traits structurally — Clone, Debug, PartialEq, Hash, Default, and others — saving you the boilerplate of writing each impl by hand. Deriving Debug lets you print a value with the {:?} specifier (or {:#?} for pretty, multi-line output) in println!/format!, which is the standard way to dump a struct for debugging. Note {} (Display) is not auto-derivable — you must implement Display yourself, because the human-facing representation is a deliberate design choice, whereas Debug's programmer-facing one can be generated mechanically.

Rust/error-handling/question-operator

Inside fn load() -> Result<Config, io::Error>, you call let s = fs::read_to_string(path)?;. What does the ? operator do when read_to_string returns Err(e)?#

Options

Show answer

On Err(e), ? returns early from the enclosing function, propagating the error to the caller after converting it via the From trait into the function's declared error type. On Ok(v) it unwraps to v and continues. Unlike unwrap, ? never panics — it propagates. The From conversion is what lets one ? bridge many underlying error types into a single return error.

Why:

On Err(e), ? returns from the enclosing function immediately with that error, after converting it via the From trait into the function's declared error type — here both sides are io::Error, so no conversion is needed. On Ok(v) it unwraps to v and execution continues. ? is not unwrap: it never panics (that's option b's mistake); it propagates. The From conversion is the key feature — it lets you write ? across many underlying error types as long as your function's error type implements From for each, which is why Box<dyn Error> or a custom error enum with #[from] derives are common return types. ? only works in functions that return Result, Option, or another Try type.

Rust/collections-iterators/iterators

Rust iterator adapters like .map() and .filter() are lazy: calling v.iter().map(f).filter(g) does no work until a consuming operation (such as .collect(), .sum(), or a for loop) drives the chain.#

Options

Show answer

True. Iterator adapters like map and filter are lazy — each returns a new iterator wrapping the previous one and touches no element until a consumer (collect, sum, for, etc.) drives the chain. A chain with no consumer does nothing, and the compiler warns about it. Laziness is what makes adapter chains zero-cost: the compiler fuses them into one tight loop with no intermediate allocations.

Why:

True. Adapters such as map, filter, take, and enumerate are lazy — each just returns a new iterator struct that wraps the previous one; no element is touched until a consumer pulls values through. Consumers include for loops and methods like collect, sum, count, fold, and for_each. A direct consequence: a chain of adapters with no consumer is a no-op (the compiler even warns iterators are lazy and do nothing unless consumed). Laziness is also what makes adapter chains a 'zero-cost abstraction' — the compiler fuses the whole chain into a single tight loop with no intermediate collections, so iter().map().filter().sum() allocates nothing in between and is as fast as a hand-written loop.

Rust/collections-iterators/pattern-matching

What does this Rust program print?#

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("{}", x);
    }
    println!("{}", x);
}

Options

Show answer
12
6
Why:

This is shadowing, not mutation. Each let x = ... introduces a brand-new immutable binding that happens to reuse the name x, hiding the previous one — no mut is needed and there is no compile error (option d). In main's scope, x becomes 5, then 6. The inner block shadows again with x * 2 = 12 and prints 12; that inner binding is scoped to the block, so when it ends the outer x (still 6) is visible again and the second println! prints 6. Output: 12 then 6. Shadowing differs from mut reassignment in two ways that matter: it can change the variable's type (let s = s.len();), and the original value isn't altered — useful for staged transformations of an input.

Rust/error-handling/panic-unwrap

When is .unwrap() (or .expect()) on a Result/Option acceptable, and what should you use instead in code that must not crash?#

Show answer

unwrap() returns the inner value on Ok/Some but panics on Err/None, aborting the current thread. It's acceptable in throwaway prototypes, examples, tests, and cases where the value is provably present (an invariant you can prove can never fail), where a panic signals a genuine bug. expect("msg") is preferable to unwrap() even then because it documents the invariant in the panic message. In production code paths that must not crash, propagate the error instead: use the ? operator to bubble it up, match/if let to handle both arms, or combinators like map, and_then, unwrap_or, unwrap_or_else, and ok_or to supply a fallback or convert between Result and Option. The principle is that recoverable errors should be returned as values, and panics reserved for unrecoverable, programmer-error situations.

Why:

unwrap/expect panic on the error case, so they're fine in tests, prototypes, and spots where the success case is a guaranteed invariant — and expect is the better of the two because its message documents why the call can't fail. Production code that must stay up should instead propagate the error with ?, branch on it with match/if let, or fall back with combinators like unwrap_or_else. A strong answer ties this to Rust's split between recoverable errors (returned as Result) and unrecoverable bugs (panics). The real-world cost of careless unwrap is a service that crashes on the first malformed input.

Rust/memory-model/box

What is Box<T> and what are the main reasons to reach for it?#

Show answer

Box<T> is the simplest smart pointer: it owns a single heap allocation holding a value of type T, while the Box itself is a pointer-sized handle on the stack. When the Box goes out of scope it drops the value and frees the heap memory automatically (RAII). The three classic reasons to use it: (1) to give a recursive type a known, finite size — e.g. enum List { Cons(i32, Box<List>), Nil }, where boxing breaks the otherwise-infinite size; (2) to store a large value on the heap and move it cheaply by transferring just the pointer instead of copying the whole value; and (3) to hold a trait object whose size isn't known at compile time, such as Box<dyn Error> or Vec<Box<dyn Shape>>, enabling dynamic dispatch over heterogeneous types. Box<T> has single ownership and zero runtime overhead beyond the heap allocation itself.

Why:

Box<T> is owned, single-owner heap allocation with automatic cleanup on drop. The interview-critical use cases are: enabling recursive types (which would otherwise have infinite size), cheaply moving large values by pointer, and holding unsized trait objects for dynamic dispatch. A common follow-up is contrasting it with Rc/Arc (shared ownership) — Box is the one-owner case with no reference counting. Knowing the recursive-type use (Box<List>) is the detail that separates a memorized answer from real understanding.

Rust/memory-model/drop-raii

Three local values are created in one scope: let a = Guard("a"); let b = Guard("b"); let c = Guard("c"); where Guard prints its name in its Drop impl. Order the four events from when the scope ends, top to bottom, in the sequence they actually occur.#

Put these in order

Show answer

Rust drops locals in reverse declaration order — last-in, first-out — when the scope ends. So after execution reaches the closing brace, the order is: c drops first (it was declared last), then b, then a (declared first, dropped last). This LIFO unwinding keeps earlier values valid in case later ones borrow from them. Note that struct fields instead drop in declaration order, top to bottom.

Why:

At the end of a scope, Rust drops local variables in reverse order of declaration — last-in, first-out — so c drops first, then b, then a. This LIFO order exists because a later value may borrow from or depend on an earlier one, so unwinding in reverse keeps those dependencies valid while each Drop runs. (Fields within a single struct, by contrast, drop in declaration order, top to bottom.) Knowing the order matters for RAII guards: if a MutexGuard and a value that uses it are in the same scope, their declaration order decides which lock is released first, and getting it wrong can deadlock or release a resource too early.

Rust/concurrency/send-sync

Send means a type can be moved to another thread; Sync means &T can be shared across threads. Which of these types can safely be sent or shared across threads as written? Select all that apply.#

Options

Pick every one that applies.

Show answer

Arc<i32> (Send + Sync), plain i32 (Send), and Mutex<Vec<u8>> (Sync, so shareable by reference) are all safe across threads. Rc<i32> is deliberately not Send because its count is non-atomic, and RefCell<i32> is not Sync because its borrow flags aren't atomic — sharing either across threads is a compile error. Send/Sync are auto-derived marker traits the compiler uses to rule out data races.

Why:

Arc<i32> is Send + Sync because its count is atomic, so it can move to another thread (a). Plain i32 is Send (and Sync), so it crosses freely (e). Mutex<Vec<u8>> is Sync when its contents are Send, so &Mutex<...> can be shared across threads — that's the whole point of a mutex (c). Rc<i32> is explicitly not Send: its non-atomic count would race, so it cannot be sent to another thread (b is wrong). RefCell<i32> is Send but not Sync: its runtime borrow flags aren't atomic, so a shared &RefCell across threads could double-mutably-borrow undetected — Rust forbids it (d is wrong). These two marker traits are auto-derived structurally, and the borrow checker uses them to make data races a compile error rather than a runtime bug.

Rust/ownership/borrow-checker

This function fails to compile. What is the root cause the borrow checker is reporting?#

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{}", first);
}

Options

Show answer

first is an immutable borrow of v that is still used after v.push(4); push needs a &mut borrow, and Rust forbids a mutable borrow while a shared borrow is live (push could reallocate and dangle first)

Why:

let first = &v[0] takes a shared (immutable) borrow of v, and that borrow is still live because first is used in the final println!. v.push(4) requires a mutable borrow of v, but Rust's aliasing rule forbids a &mut while any & is outstanding. This isn't pedantry: push may reallocate the Vec's backing buffer when capacity is exceeded, which would leave first pointing at freed memory — a classic iterator-invalidation / dangling-pointer bug that the borrow checker turns into a compile error instead. Option b is backwards (mut is required for push); the real fix is to use first before mutating, or copy the value out (let first = v[0];, since i32 is Copy) so no borrow outlives the mutation.

Related interview questions

The other 79 questions

This page shows 10. A free account opens the rest and marks your answers, which a page of answers can't do. Miss one and it returns a few days later, then a week after that, until you stop missing it.

Start free

Free · the whole bank · 100 graded answers per 30 days · written feedback and job-ad quizzes on the paid plan

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.