Rust interview questions: a hands-on guide for engineers
When you step into a Rust role, interviewers aren't just testing syntax knowledge—they're evaluating whether you understand the language's core philosophy around memory safety and concurrency. Rust interview questions rarely ask you to recite documentation; instead, they probe your grasp of ownership, borrowing, thread safety, and error handling through scenarios you'll actually face in production. This guide walks you through the most common Rust interview questions that mid-level and senior engineers encounter, with hands-on examples that clarify not just the "what" but the "why."
Understanding ownership
Ownership is the bedrock of Rust's memory model. Every value in Rust has exactly one owner, and when that owner leaves scope, the value is dropped automatically. This eliminates the need for garbage collection while preventing use-after-free bugs. In interviews, you'll often get asked to explain ownership in the context of moving data or returning values from functions.
Let's consider a real scenario: you're building a web service that processes user profiles. Each profile struct is owned by one variable at a time.
struct UserProfile {
id: u64,
name: String,
email: String,
}
fn process_profile(profile: UserProfile) {
println!("Processing user: {}", profile.name);
// ownership of profile moves into this function
// profile is dropped here when the function ends
}
fn main() {
let user = UserProfile {
id: 1,
name: String::from("Alice"),
email: String::from("alice@example.com"),
};
process_profile(user);
// user no longer exists here—ownership was moved
// This line would fail: println!("{}", user.name);
}The key insight: when you pass user to process_profile, ownership transfers completely. The original variable can no longer be used. Interviewers expect you to recognize this and explain why Rust does this—it prevents double-free errors and makes resource cleanup predictable.
When asked about ownership in an interview, mention that it forces you to think about data flow. You can't accidentally use a value after it's been freed because the compiler catches it at compile time. This is why Rust code is often safer than C or C++ without requiring a runtime garbage collector.
The borrowing rules
Borrowing lets you reference data without taking ownership. Rust enforces two simple rules: you can have either many immutable references to the same data, or exactly one mutable reference. This prevents data races at compile time. Interview questions often test whether you can spot violations of these rules or explain why a piece of code won't compile.
Here's a common scenario from e-commerce systems where multiple components need to read the same shopping cart data:
struct ShoppingCart {
items: Vec<String>,
total: f64,
}
fn display_items(cart: &ShoppingCart) {
// immutable borrow: we can only read
for item in &cart.items {
println!("{}", item);
}
}
fn apply_discount(cart: &mut ShoppingCart, discount: f64) {
// mutable borrow: we can modify
cart.total -= discount;
}
fn main() {
let mut cart = ShoppingCart {
items: vec![String::from("Laptop"), String::from("Mouse")],
total: 1500.0,
};
// Multiple immutable borrows are fine
display_items(&cart);
display_items(&cart);
// Mutable borrow requires exclusive access
apply_discount(&mut cart, 100.0);
// Can't do this: mutable borrow while immutable exists
// let read_ref = &cart;
// apply_discount(&mut cart, 50.0); // error!
}Interviewers want to see that you understand why this rule exists. Data races happen when one thread modifies data while another reads it. By preventing simultaneous mutable and immutable references, Rust eliminates this class of bug entirely. When explaining borrowing in an interview, emphasize that the compiler is your ally—if code doesn't compile due to borrowing violations, that's a safety guarantee, not an inconvenience.
A common follow-up question: "How do you work around borrowing limitations?" The answer usually involves clone() for independent copies, interior mutability patterns like RefCell or Mutex for shared mutable state, or redesigning ownership structure. Know at least one pattern well.
Send and Sync traits
These marker traits define thread safety in Rust. Send means a type can be safely moved across thread boundaries; Sync means a type can be safely shared (referenced) across threads. Most types implement both automatically, but understanding when and why they don't is important for systems and concurrent programming roles.
Consider a CRM system that processes customer records in parallel:
use std::thread;
use std::rc::Rc;
use std::sync::Arc;
struct Customer {
id: u64,
name: String,
}
fn process_customers_with_rc(customers: Vec<Customer>) {
// This won't work because Rc is not Send
// let shared = Rc::new(customers);
// thread::spawn(move || {
// println!("{:?}", shared);
// });
}
fn process_customers_with_arc(customers: Vec<Customer>) {
// Arc is both Send and Sync, so this works
let shared = Arc::new(customers);
let shared_clone = Arc::clone(&shared);
let handle = thread::spawn(move || {
println!("Thread owns Arc");
});
handle.join().unwrap();
}Rc (reference counting) is not Send because its internal reference count isn't atomic—if two threads tried to decrement the count simultaneously, the count could become corrupted. Arc (atomic reference counting) uses atomic operations, so it's safe to send across threads.
In an interview, explain that Send and Sync are compile-time guarantees. You rarely implement them manually; the compiler derives them for you. But knowing what breaks them (like non-atomic counters or Cell) shows you understand the threading model. Senior roles often test this because incorrect assumptions about thread safety cause production bugs.
Result vs panic
This is about error handling philosophy. Result<T, E> represents recoverable errors that the caller might handle. panic! is for unrecoverable errors or programmer bugs. Choosing the right one is crucial for building reliable systems. Interview questions often ask: "When would you panic versus return a Result?"
Here's a practical example from a payment processing system:
use std::num::ParseIntError;
#[derive(Debug)]
enum PaymentError {
InvalidAmount(String),
ProcessingFailed(String),
InsufficientFunds,
}
// Returns Result for expected failure modes
fn validate_and_process_payment(amount_str: &str, balance: f64) -> Result<f64, PaymentError> {
let amount: f64 = amount_str
.parse()
.map_err(|_| PaymentError::InvalidAmount(format!("'{}' is not a valid amount", amount_str)))?;
if amount <= 0.0 {
return Err(PaymentError::InvalidAmount(String::from("Amount must be positive")));
}
if amount > balance {
return Err(PaymentError::InsufficientFunds);
}
Ok(balance - amount)
}
// Panics for bugs in our own logic
fn calculate_fee_percentage(tier: &str) -> f64 {
match tier {
"standard" => 0.02,
"premium" => 0.01,
"enterprise" => 0.005,
other => panic!("Unknown tier: {} — this should never happen", other),
}
}
fn main() {
// Caller decides how to handle payment errors
match validate_and_process_payment("abc", 1000.0) {
Ok(new_balance) => println!("Payment succeeded, new balance: {}", new_balance),
Err(PaymentError::InvalidAmount(msg)) => eprintln!("Invalid input: {}", msg),
Err(PaymentError::InsufficientFunds) => eprintln!("Declined: insufficient funds"),
Err(PaymentError::ProcessingFailed(msg)) => eprintln!("Processing error: {}", msg),
}
// Panic for programmer error—we control the tier values
let fee = calculate_fee_percentage("standard");
}The rule: use Result for anything the caller might reasonably handle—network failures, invalid input, missing resources. Use panic! only for bugs that indicate broken invariants in your code. Interviewers want to see that you use unwrap() and expect() judiciously, only where you're truly certain the operation can't fail.
A senior engineer explains error handling by discussing error propagation strategies: matching, the ? operator, map_err for transformation. Show that you think about what the caller can do with the error information you provide.
Common mistakes
Several patterns trip up candidates in Rust interviews:
- Forgetting that borrowing rules apply to function parameters. When you pass a reference to a function, you still can't create a mutable borrow of the same data elsewhere. The compiler enforces this across function boundaries.
- Panicking in library code. If you're writing a library or service component, returning
Resultgives callers the choice of how to handle errors. Panicking removes their agency and can crash the whole system. - Misunderstanding Clone.
Clonemakes a deep copy; it's not free. In performance-critical code, ask yourself if you really need to clone or if borrowing would work. - Assuming Sync for shared mutable state. A type can be
Syncbut still require synchronization (likeMutex) to actually safely share mutable data.Syncjust means the type is thread-safe; you still need to use appropriate primitives. - Overusing unwrap in production code. It's fine in tests or examples, but production services should handle errors gracefully.
Key takeaways
- Ownership forces you to think about data flow early; the compiler prevents entire classes of memory bugs.
- The borrowing rules—many immutable references or one mutable—eliminate data races at compile time.
SendandSyncare compile-time guarantees about thread safety; know whyRcisn'tSendbutArcis.Resultfor recoverable errors gives callers control;paniconly for programmer bugs.- Interviewers test these concepts through code review scenarios and "will this compile?" questions—practice both.
- Demonstrate that you write Rust not just to pass the compiler, but to make your code easier to reason about and safer in production.






Comments
Loading comments…