Top 50 Rust Interview Questions and Answers (2025 Edition)
After reviewing hundreds of Rust interviews across startups and big tech, I’ve noticed a pattern: most interview guides focus on theory, but real interviews test your ability to think in Rust. This guide bridges that gap.
Whether you’re interviewing at a systems programming shop, a blockchain startup, or a web services company, these 50 questions cover what you’ll actually encounter. I’ve included not just answers, but the why behind them — the insights that separate candidates who’ve memorized concepts from those who truly understand Rust.
How to Use This Guide
- Junior roles: Focus on Fundamentals (1–15) and key Intermediate questions (16, 17, 21, 22)
- Mid-level roles: Master Fundamentals and Intermediate (1–30), skim Advanced
- Senior roles: Know everything, especially Advanced (31–45) and the practical patterns (46–50)
Part I: Fundamentals (1–15)
These questions test whether you understand Rust’s core philosophy
1. What makes Rust different from other systems languages?
Rust guarantees memory safety without garbage collection through its ownership system. Unlike C/C++ where you manually manage memory (and can easily create bugs), and unlike Go/Java where a GC pauses your program unpredictably, Rust enforces correctness at compile time with zero runtime cost. Why this matters in interviews: This tests whether you understand Rust’s value proposition, not just its syntax.
2. Explain ownership in one sentence, then elaborate.
One sentence: Each value has exactly one owner, and when that owner goes out of scope, the value is automatically dropped.
Elaboration: Ownership solves the age-old question of “who’s responsible for freeing this memory?” The three rules are:
- Each value has one owner
- Only one owner at a time
- When the owner goes out of scope, the value is dropped
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK
}
Interview tip: If asked about this, walk through an example showing a move. Many candidates just recite the rules without demonstrating understanding.
3. What is borrowing and what are the borrowing rules?
Borrowing lets you reference data without taking ownership. The rules prevent data races at compile time:
- You can have any number of immutable references (&T)
- OR exactly one mutable reference (&mut T)
- References must not outlive the data they point to
fn main() {
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK - multiple immutable borrows
println!("{} and {}", r1, r2);
let r3 = &mut s; // OK - r1, r2 no longer used
r3.push_str(" world");
}
Common mistake: Trying to mix mutable and immutable borrows in the same scope.
4. What’s the difference between String and &str?
- String: Owned, heap-allocated, growable. Like std::string in C++ or StringBuilder in Java.
- &str: Borrowed string slice, immutable view into UTF-8 data. Points to data owned elsewhere.
fn greet(name: &str) { // Accepts both &str and &String
println!("Hello, {}", name);
}
fn main() {
let owned = String::from("Alice");
let slice = "Bob"; // &str literal
greet(&owned); // String coerces to &str
greet(slice); // Direct &str
}
Best practice: Function parameters should almost always use &str for flexibility.
5. How does Rust handle null values?
Rust has no null. Instead, it uses Option<T>:
enum Option<T> {
Some(T),
None,
}
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
match divide(10, 2) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero"),
}
}
Why interviewers ask this: Testing whether you understand how Rust enforces explicit error handling.
6. What is Result<T, E> and when do you use it?
Result represents operations that can fail with specific error types:
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // ? propagates errors
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Rule of thumb: Use Option for absence of value, Result for operations that can fail.
7. Explain the ? operator.
The ? operator is syntactic sugar for error propagation. It:
- Unwraps Ok values
- Returns Err early
- Automatically converts error types using From
// Without ?
fn old_style() -> Result<String, std::io::Error> {
let file = match File::open("file.txt") {
Ok(f) => f,
Err(e) => return Err(e),
};
// ... more code
}
// With ?
fn new_style() -> Result<String, std::io::Error> {
let file = File::open("file.txt")?;
// ... more code
}
8. What types implement Copy and why does it matter?
Copy types are duplicated on assignment rather than moved:
- Copy: Integers, floats, bool, char, tuples/arrays of Copy types
- Not Copy: String, Vec<T>, Box<T>, types with heap allocation
let x = 5;
let y = x; // x is copied, both valid
let s1 = String::from("hello");
let s2 = s1; // s1 is moved, no longer valid
Interview insight: Understanding Copy vs move semantics is fundamental to predicting Rust's behavior.
9. What’s the difference between Copy and Clone?
- Copy: Implicit, cheap bitwise copy (stack only)
- Clone: Explicit (.clone()), can be expensive (heap allocation)
#[derive(Clone)]
struct Person {
name: String,
age: u32,
}
let p1 = Person { name: "Alice".to_string(), age: 30 };
let p2 = p1.clone(); // Explicit deep copy
// Both p1 and p2 valid
10. What is pattern matching and why is it powerful? Pattern matching destructures data and ensures exhaustiveness:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
}
// Compiler ensures all variants handled
}
11. What are traits and how do they enable polymorphism?
Traits define shared behavior, similar to interfaces:
trait Drawable {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing circle with radius {}", self.radius);
}
}
impl Drawable for Square {
fn draw(&self) {
println!("Drawing square with side {}", self.side);
}
}
fn render(shape: &dyn Drawable) { // Dynamic dispatch
shape.draw();
}
12. What’s the difference between impl Trait and dyn Trait?
- impl Trait (static dispatch): Compile-time polymorphism, zero-cost
- dyn Trait (dynamic dispatch): Runtime polymorphism, vtable overhead
// Static dispatch - compiler generates specific code
fn make_iter() -> impl Iterator<Item = i32> {
0..10
}
// Dynamic dispatch - runtime flexibility
fn store_shapes() -> Vec<Box<dyn Drawable>> {
vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Square { side: 3.0 }),
]
}
When to use which: Static dispatch for performance, dynamic dispatch for heterogeneous collections.
13. What is panic! and when should you use it? panic! crashes the program for unrecoverable errors:
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!"); // Programmer error
}
a / b
}
Best practice: Use panic! for bugs/invariants, Result for expected failures.
14. Explain shadowing vs mutability.
// Shadowing - can change type let x = 5; let x = x + 1; let x = x * 2; let x = "now a string"; // Different type OK // Mutability - same type only let mut y = 5; y = y + 1; // y = "string"; // ERROR: type mismatch
15. What is Cargo and why is it essential?
Cargo is Rust’s build system and package manager:
- cargo new - creates projects
- cargo build - compiles code
- cargo test - runs tests
- cargo doc - generates documentation
- Manages dependencies via Cargo.toml
Part II: Intermediate Concepts (16–35)
These questions test deeper understanding and practical experience
16. What are lifetimes and why are they necessary? Lifetimes are compile-time annotations that prevent dangling references:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
} // ERROR: s2 doesn't live long enough
}
Key insight: Lifetimes don’t change how long data lives — they ensure references are valid.
17. What is lifetime elision?
Rust infers lifetimes in common patterns:
// Explicit lifetime
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap()
}
// Elided (compiler infers)
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap()
}
18. What are smart pointers? Name three common ones.
Smart pointers provide additional functionality beyond regular references:
1. Box<T> - Heap allocation
let b = Box::new(5); // Allocates i32 on heap
2. Rc<T> - Reference counting (single-threaded)
use std::rc::Rc; let a = Rc::new(5); let b = Rc::clone(&a); // Both point to same data
3. Arc<T> - Atomic reference counting (thread-safe)
use std::sync::Arc; let a = Arc::new(5); let b = Arc::clone(&a); // Can be shared across threads
19. When do you use Box<T>?
Three main use cases:
1. Recursive types
enum List {
Cons(i32, Box<List>),
Nil,
}
2. Large data transfer without copying
let large_array = Box::new([0; 1_000_000]);
3. Trait objects
let shape: Box<dyn Drawable> = Box::new(Circle { radius: 5.0 });
20. What is interior mutability and why is it needed? Interior mutability allows mutation through immutable references:
use std::cell::RefCell;
struct Dashboard {
data: RefCell<Vec<i32>>,
}
impl Dashboard {
fn update(&self, value: i32) { // Takes &self, not &mut self
self.data.borrow_mut().push(value);
}
}
When to use: When you need mutation but can’t get &mut (e.g., callbacks, shared state).
21. Explain Cell<T> vs RefCell<T>.
- Cell<T>: For Copy types, replaces entire value
- RefCell<T>: For any type, runtime borrow checking
use std::cell::{Cell, RefCell};
let c = Cell::new(5);
c.set(10); // Replace value
let r = RefCell::new(vec![1, 2, 3]);
r.borrow_mut().push(4); // Borrow and modify
22. What are Mutex<T> and RwLock<T> used for?
Thread-safe interior mutability: Mutex — Mutual exclusion
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);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
RwLock — Multiple readers or one writer
use std::sync::RwLock;
let lock = RwLock::new(5);
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap(); // Multiple readers OK
println!("{} {}", r1, r2);
}
{
let mut w = lock.write().unwrap(); // Exclusive writer
*w += 1;
}
23. What are Send and Sync traits?
Marker traits for thread safety:
- Send: Type can be transferred across thread boundaries
- Sync: Type can be referenced from multiple threads (&T is Send)
// Rc<T> is NOT Send or Sync (single-threaded only)
// Arc<T> IS Send and Sync (atomic operations)
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{:?}", data_clone);
}).join().unwrap();
Interview insight: This question tests understanding of Rust’s concurrency guarantees.
24. How do closures capture their environment?
Three ways, corresponding to the Fn traits:
let x = 5;
// Fn - borrows immutably
let read = || println!("{}", x);
// FnMut - borrows mutably
let mut y = 5;
let mut modify = || y += 1;
// FnOnce - takes ownership
let consume = move || drop(x);
25. What is the Drop trait?
Custom cleanup when values go out of scope:
struct FileHandle {
name: String,
}
impl Drop for FileHandle {
fn drop(&mut self) {
println!("Closing file: {}", self.name);
}
}
fn main() {
let _handle = FileHandle { name: "data.txt".to_string() };
// drop() called automatically at end of scope
}
26. Explain generics and monomorphization.
Generics provide type-safe abstraction without runtime cost:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Monomorphization: Compiler generates separate code for each concrete type used, eliminating generics overhead.
27. What are associated types in traits?
Associated types simplify trait definitions:
trait Iterator {
type Item; // Associated type
fn next(&mut self) -> Option<Self::Item>;
}
// vs generic alternative (more verbose)
trait IteratorGeneric<T> {
fn next(&mut self) -> Option<T>;
}
28. What is the orphan rule?
You can only implement a trait for a type if either the trait or type is local to your crate. This prevents conflicting implementations across the ecosystem.
// OK: Display is external, MyType is local
impl std::fmt::Display for MyType { ... }
// ERROR: Both Display and Vec are external
// impl std::fmt::Display for Vec<i32> { ... }
// Workaround: Newtype pattern
struct MyVec(Vec<i32>);
impl std::fmt::Display for MyVec { ... }
29. How do iterators work in Rust?
Iterators are lazy and zero-cost:
let v = vec![1, 2, 3, 4, 5];
// Chain operations (no intermediate allocations)
let result: Vec<_> = v.iter()
.filter(|&&x| x > 2)
.map(|&x| x * 2)
.collect();
println!("{:?}", result); // [6, 8, 10]
Key insight: Iterator chains compile to efficient loops with no overhead.
30. What’s the difference between .iter(), .iter_mut(), and .into_iter()?
let v = vec![1, 2, 3];
// .iter() - borrows immutably
for x in v.iter() {
println!("{}", x); // x is &i32
}
// .iter_mut() - borrows mutably
for x in v.iter_mut() {
*x *= 2; // x is &mut i32
}
// .into_iter() - takes ownership
for x in v.into_iter() {
println!("{}", x); // x is i32, v is consumed
}
31. What are declarative macros (macro_rules!)? Code that writes code via pattern matching:
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
let v = vec![1, 2, 3]; // Expands to push operations
32. What are procedural macros?
Functions that operate on token streams to generate code:
#[derive(Debug, Clone)] // Procedural macros
struct Point { x: i32, y: i32 }
// Expands to implementations of Debug and Clone
33. When should you use Vec<T> vs VecDeque<T> vs LinkedList<T>?
- Vec<T>: Default choice, fast random access, efficient push/pop at end
- VecDeque<T>: Efficient push/pop at both ends (ring buffer)
- LinkedList<T>: Rarely useful, only for specific splice operations
34. What’s the difference between HashMap and BTreeMap?
- HashMap: O(1) average operations, unordered, requires Hash
- BTreeMap: O(log n) operations, sorted keys, requires Ord
use std::collections::{HashMap, BTreeMap};
let mut hash = HashMap::new();
hash.insert("key", "value");
let mut btree = BTreeMap::new();
btree.insert(3, "three");
btree.insert(1, "one");
// Iteration yields sorted keys: 1, 3
35. How do you handle multiple error types? Option 1: Custom error enum
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
Option 2: Box dynamic error
fn do_work() -> Result<(), Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("file.txt")?;
let number: i32 = contents.trim().parse()?;
Ok(())
}
Option 3: Use anyhow or thiserror crate
Part III: Advanced Topics (36–45) These questions test expert-level knowledge
36. What is unsafe Rust and what can you do in unsafe blocks?
Five unsafe superpowers:
- Dereference raw pointers
- Call unsafe functions
- Access/modify mutable statics
- Implement unsafe traits
- Access union fields
fn dangerous() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1: {}", *r1);
*r2 = 10;
}
}
Critical: unsafe doesn't disable borrow checker—it's your responsibility to uphold safety.
37. When is unsafe necessary? Common legitimate uses:
- FFI (calling C libraries)
- Implementing low-level data structures (e.g., intrusive linked lists)
- Performance-critical code bypassing bounds checks
- Interfacing with hardware
38. Explain async/await and Futures. Async functions return state machines implementing Future:
async fn fetch_data() -> String {
// Simulated async operation
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"data".to_string()
}
#[tokio::main]
async fn main() {
let result = fetch_data().await;
println!("{}", result);
}
Key concept: await yields control to the executor, allowing other tasks to run. 39. What is Pin and why does async need it?
Pin prevents self-referential types from moving in memory:
// Simplified conceptual example
struct SelfRef {
value: String,
pointer: *const String, // Points to value
}
// If SelfRef moves, pointer becomes invalid
// Pin<Box<SelfRef>> prevents moving
Interview tip: You rarely use Pin directly, but understanding it shows async expertise.
40. How does Rust prevent data races? Compile-time enforcement via:
- Ownership — only one mutable access
- Send/Sync — controls thread safety
- Borrow checker — prevents aliasing
// This won't compile - data race prevented
let mut data = vec![1, 2, 3];
thread::spawn(|| {
data.push(4); // ERROR: data moved
});
data.push(5); // ERROR: already moved
Fixed version:
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
thread::spawn(move || {
data_clone.lock().unwrap().push(4);
}).join().unwrap();
data.lock().unwrap().push(5);
41. What are raw pointers and when do you use them?
- const T and *mut T bypass safety guarantees:
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1: {}", *r1);
*r2 = 10;
println!("r2: {}", *r2);
}
Use cases: FFI, custom allocators, lock-free data structures.
42. Explain zero-sized types (ZSTs).
Types that occupy no memory:
struct Marker; // Zero-sized let markers = vec![Marker, Marker, Marker]; // No heap allocation
Applications: Type-level programming, zero-cost markers, PhantomData.
43. What is PhantomData used for?
Indicates “phantom” ownership for types not directly stored:
use std::marker::PhantomData;
struct Slice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<&'a T>, // Acts as if we hold &'a T
}
44. How would you implement a graph in Rust?
Arena-based approach using indices:
struct Node {
value: i32,
edges: Vec<usize>, // Indices instead of references
}
struct Graph {
nodes: Vec<Node>,
}
impl Graph {
fn add_edge(&mut self, from: usize, to: usize) {
self.nodes[from].edges.push(to);
}
}
Why this works: Avoids circular references and ownership issues.
45. What are Higher-Ranked Trait Bounds (HRTBs)?
Trait bounds that work for all lifetimes:
fn call_with_ref<F>(f: F)
where
F: for<'a> Fn(&'a i32), // Works with any lifetime
{
let x = 5;
f(&x);
}
Part IV: Practical Patterns (46–50)
These questions test real-world problem-solving
46. How do you structure a large Rust project?
my-project/
├── Cargo.toml
├── src/
│ ├── main.rs # Binary entry point
│ ├── lib.rs # Library root
│ ├── config/
│ │ └── mod.rs
│ ├── api/
│ │ ├── mod.rs
│ │ └── handlers.rs
│ └── db/
│ └── mod.rs
├── tests/ # Integration tests
│ └── integration_test.rs
└── benches/ # Benchmarks
└── bench.rs
Best practices:
- Separate binary and library code
- Use workspace for multi-crate projects
- Group related modules
- Write integration tests in tests/
47. What are common performance optimization techniques?
- Use release builds — cargo build --release
- Avoid unnecessary allocations
// Bad
let s = format!("Hello {}", name);
// Good
let s = format_args!("Hello {}", name);
3. Leverage iterators — compiler optimizes them aggressively
4. Use &str over String when possible
5. Profile before optimizing — use cargo flamegraph
6. Consider SmallVec for small collections
7. Use Cow for clone-on-write
48. How do you test Rust code effectively?
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_addition() {
assert_eq!(2 + 2, 4);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_panic() {
divide(10, 0);
}
#[test]
fn test_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err("Math is broken".to_string())
}
}
}
Integration tests:
// tests/integration_test.rs
use my_crate;
#[test]
fn full_workflow() {
// Test public API
}
49. What is the newtype pattern and when do you use it? Wrapping types for type safety:
struct UserId(u64);
struct OrderId(u64);
fn process_user(id: UserId) {
// ...
}
// This won't compile (type safety)
let order = OrderId(123);
// process_user(order); // ERROR
Benefits: Zero runtime cost prevents mixing similar types, custom trait implementations.
50. Describe your approach to error handling in a production application. Library code: Define custom error types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection failed: {0}")]
ConnectionError(String),
#[error("Query failed: {0}")]
QueryError(String),
}
Application code: Use anyhow for flexibility
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = load_config()
.context("Failed to load configuration")?;
run_app(config)
.context("Application error")?;
Ok(())
}
Best practices:
- Use Result consistently
- Add context to errors
- Log errors at boundaries
- Create custom error types for domain logic
- Use anyhow for applications, thiserror for libraries
Conclusion: Mastering Rust Interviews
The key to acing Rust interviews isn’t memorizing definitions — it’s developing intuition for how ownership, borrowing, and lifetimes solve real problems. Here’s your action plan:
Week 1: Master fundamentals (1–15). Write small programs demonstrating each concept.
Week 2–3: Practice intermediate concepts (16–35). Implement a CLI tool or web server.
Week 4: Dive into advanced topics (36–45) and study real-world Rust codebases. Ongoing: Solve problems on Exercism.io, read The Rust Book, and contribute to open source.
Resources
- The Rust Book — Official guide
- Rust by Example — Hands-on examples
- Rustlings — Interactive exercises
- Tokio Tutorial — For async programming
Final Thoughts Rust interviews test not just knowledge, but problem-solving ability. When you encounter a question, you haven’t seen before, think through the ownership implications, consider the performance trade-offs, and explain your reasoning clearly.
Remember: The compiler is your ally. If you can explain why the code doesn’t compile, you understand Rust at a deep level — and that’s what interviewers are really looking for.
Good luck with your interviews! If you found this helpful, consider sharing it with others preparing for Rust roles.