Rust Interview Questions and Answers

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

Practise 10 random 10 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 What is ownership in Rust and why does the language need it? Easy

Ownership is Rust's compile-time memory management model. Every value has exactly one owner, and when the owner goes out of scope the value is dropped and its memory freed. There is no garbage collector.

The three rules are: each value has a single owner, there can only be one owner at a time, and when the owner goes out of scope the value is dropped.

fn main() {
    let s = String::from("hello");
    let t = s;              // s is moved into t
    // println!("{}", s);   // error: borrow of moved value
    println!("{}", t);
}

Simple types such as integers implement Copy and are copied rather than moved. Moving prevents double frees and use-after-free at compile time. When you genuinely need multiple owners, reach for reference counting with Rc or Arc. Functions can take ownership, borrow with &, or return ownership. This discipline is why Rust can guarantee memory safety without a runtime manager, at the cost of thinking about ownership in every API.

2 What is the difference between String and &str in Rust? Easy

String is an owned, growable, heap-allocated UTF-8 string (essentially a Vec<u8>) that you can mutate and extend. &str is a borrowed string slice: a pointer plus a length into UTF-8 data that may live in the binary, on the stack, or inside a String. It is immutable.

let owned: String = String::from("hi");
let slice: &str = &owned;       // borrow
let literal: &str = "static";   // &'static str

fn greet(name: &str) {
    println!("{name}");
}
greet(&owned);                  // deref coercion String -> &str

Prefer &str for function parameters because it accepts both String (through deref coercion) and string literals, making the API more flexible. Use String when the function must own or modify the data. Concatenation with + consumes the left-hand String, and format! is often clearer. &String can be coerced to &str, but &str cannot become &String, which is why libraries expose slices.

3 What does Cargo do for a Rust project? Easy

Cargo is Rust's build system and package manager. It handles dependency resolution, compilation, testing, documentation, benchmarking and publishing, which keeps projects consistent.

  • Cargo.toml declares the package: name, version, edition, dependencies, features and build profiles.
  • Cargo.lock pins exact dependency versions for reproducible builds. Commit it for binaries, not necessarily for libraries.
  • src/main.rs builds a binary crate; src/lib.rs builds a library crate. Dependencies come from crates.io, a git URL or a local path.
[package]
name = "demo"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }

Common commands are cargo build, cargo run, cargo test, cargo clippy for lints, cargo fmt and cargo doc. Workspaces let several crates share one lockfile and target directory. This tooling is a major reason Rust projects look uniform across teams.

4 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.

5 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.

6 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.

7 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.

8 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.

9 When would you use Rc, Arc, RefCell or Mutex? Hard

Ownership allows one owner, but graphs and shared state need more. Rust pushes the choice into types.

  • Rc<T>: single-threaded reference counting. clone increments a count and creates another owner; not atomic.
  • Arc<T>: atomic reference counting, safe to share across threads.
  • RefCell<T>: runtime-checked interior mutability for one thread. Borrowing rules are enforced at runtime and violations panic.
  • Mutex<T> and RwLock<T>: cross-thread interior mutability. Locking returns a guard and poisoning is tracked.
use std::rc::Rc;
use std::cell::RefCell;

let shared = Rc::new(RefCell::new(vec![1]));
let alias = Rc::clone(&shared);
alias.borrow_mut().push(2);

Combine Rc<RefCell<T>> for single-threaded shared mutation and Arc<Mutex<T>> across threads. Rc and RefCell are not Send, so the compiler prevents using them across threads. Use Rc::downgrade to create a Weak reference that breaks reference cycles which would otherwise leak.

10 How do async/await, futures, Send and Sync fit together? Hard

An async fn returns a Future, a state machine that does nothing until polled. An executor such as Tokio drives it. .await yields control while the future is pending, so one thread can juggle many tasks.

async fn fetch(url: &str) -> reqwest::Result<String> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

Futures do not allocate an OS stack each, so they are far cheaper than threads and let you run very many concurrent operations. The cost is a runtime dependency you must choose and configure.

Send means a value can move across threads; Sync means it can be shared by reference. Futures spawned onto a multi-threaded runtime must be Send, which is why holding a non-Send value such as an Rc or a std::MutexGuard across an .await fails to compile. Use tokio::sync::Mutex in async code and run blocking work on spawn_blocking. 'static bounds on spawned futures catch borrowing mistakes early.

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.