Rust Interview Questions and Answers

Ownership, borrowing, lifetimes, traits and the Cargo toolchain.

Practise 10 random 5 peer-reviewed questions
Rust Interview Syllabus & Preparation Strategy

Whether you are preparing for entry-level Rust interview questions for freshers or senior software engineer interview questions addressing concurrency, scalability, and system architecture, this track provides peer-reviewed model answers with syntax walkthroughs, edge cases, and practical interview tips.

1 Explain the borrow checker with an example. Medium

The borrow checker enforces the borrowing rules at compile time: you may have either one mutable reference or any number of immutable references, and references must always be valid.

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];   // immutable borrow
    v.push(4);           // error: cannot borrow as mutable
    println!("{first}"); // first is still used here
}

The immutable borrow is still alive at the push, so Rust rejects the program. Fixes include copying the value with first.copied(), letting the borrow end before mutating, or reordering the statements.

Another classic case is returning a reference to a local variable: the borrow checker sees that it would dangle and refuses to compile. This eliminates use-after-free, iterator invalidation and data races without runtime cost. When the checker feels too strict, reach for indices, cloning or a restructure so borrows do not overlap rather than sprinkling unsafe.

2 What are lifetimes in Rust and when do you need to annotate them? Medium

Lifetimes are generic annotations that describe how long references remain valid, so the compiler can prove they never dangle. Most lifetimes are elided and invisible in everyday code.

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

Here 'a means the returned reference lives no longer than the shorter of the two inputs. The 'static lifetime means the data lives for the whole program, as with string literals.

Elision rules: each input reference gets its own lifetime; if there is exactly one input lifetime, the output gets it; if one input is &self, the output gets the self lifetime.

You must annotate when the compiler cannot infer the relationship, for example when returning one of several references or when a struct holds references: struct Parser<'a> { input: &'a str }. Lifetimes change no runtime behaviour; they are purely a compile-time proof obligation.

3 How do traits compare with interfaces in other languages? Medium

A trait defines shared behaviour: a set of method signatures a type can implement. Implementations can be written for types you did not define, as long as you own the trait, which is the orphan rule.

trait Area {
    fn area(&self) -> f64;
}

impl Area for Circle {
    fn area(&self) -> f64 { 3.14159 * self.r * self.r }
}

Traits support default method bodies, associated types and constants, and generic bounds such as fn f<T: Area>(t: T). Dispatch comes in two flavours:

  • Static dispatch with generics or impl Trait, monomorphised at compile time, with zero overhead.
  • Dynamic dispatch with dyn Trait, a fat pointer carrying a vtable, needed for heterogeneous collections.

Rust has no class inheritance; you compose traits and use supertraits for dependencies. Trait objects must be object-safe, which excludes generic methods and associated constants. Traits plus enums are also the idiomatic replacement for many inheritance hierarchies.

4 How do Option, Result and the question mark operator work? Medium

Option<T> models presence with Some and None, replacing null. Result<T, E> models success or failure with Ok and Err, and is how Rust does recoverable errors.

fn parse(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n: i32 = s.trim().parse()?; // returns early on Err
    Ok(n * 2)
}

The ? operator unwraps Ok and returns the error early, converting through the From trait, which keeps propagation clean. Combinators such as map, and_then, unwrap_or and ok_or let you work without a full match.

unwrap() and expect() panic on failure and belong in tests or when failure is truly impossible. For libraries, define an error enum or use thiserror; for applications anyhow provides convenient boxed errors. This design makes failure explicit in the type system, so callers cannot forget to consider it, unlike exceptions that may be silently swallowed.

5 How do you implement a custom iterator in Rust? Medium

You implement the Iterator trait by providing next, which returns Option<Self::Item>.

struct Counter { count: u32, max: u32 }

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

let sum: u32 = Counter { count: 0, max: 5 }.sum();

The trait provides dozens of adapters such as map, filter, take, zip and fold. They are lazy: nothing runs until a consuming method like collect, sum or a for loop drives the iterator. This laziness plus monomorphisation produces efficient pipelines that often compile to the same code as a hand-written loop.

Implement size_hint when the length is known to help collect preallocate, and implement ExactSizeIterator or DoubleEndedIterator when the guarantees hold. Iterators are the idiomatic way to process sequences in Rust.

Frequently Asked Questions About Rust Interviews

What do hiring managers evaluate in Rust technical rounds?

Technical interviewers look for foundational fluency, idiomatic syntax, clarity when communicating complex logic, and awareness of performance trade-offs (e.g. memory footprint, render performance, and network latency) in production environments.

What are the best interview tips for practicing Rust questions?

Use active recall: summarize each answer in your own words before revealing the model solution. Focus on explaining why a certain approach is chosen rather than just memorizing code syntax.