# Rust Interview Questions 2025 — Ownership, Borrowing, Lifetimes, Traits, Async/Await, Error Handling, Concurrency

Source: https://www.techinterview.org/post/3233474457/rust-interview-questions-2025-ownership-borrowing-lifetimes-traits-async-await-error-handling-smart-pointers-concurrency/
Updated: 2026-04-21 · techinterview.org

Rust is the language of systems programming, WebAssembly, and performance-critical applications. Used at AWS (Firecracker), Cloudflare (Workers), Discord, and Dropbox. Rust interviews focus on ownership/borrowing (the defining feature), lifetime management, trait-based polymorphism, and fearless concurrency. This guide covers what you need for Rust engineering interviews.

## Ownership and Borrowing

Ownership rules: (1) Each value has exactly one owner. (2) When the owner goes out of scope, the value is dropped (freed). (3) Ownership can be transferred (moved): let s2 = s1; -- s1 is no longer valid. These rules eliminate: use-after-free, double-free, and memory leaks -- at compile time, with zero runtime cost. Borrowing: instead of transferring ownership, lend a reference. Immutable borrow: &T -- multiple readers allowed. Mutable borrow: &mut T -- exclusive access (no other borrows exist). The borrow checker enforces at compile time: you can have EITHER multiple immutable references OR one mutable reference (not both). This prevents data races at compile time. fn longest(x: &str, y: &str) -> &str -- borrows two strings, returns a reference to one. The compiler verifies the returned reference does not outlive the borrowed data. Move semantics: let v = vec![1,2,3]; let v2 = v; -- v is moved to v2. Using v after this is a compile error. For types implementing Copy (integers, booleans, floats): assignment copies instead of moving. For heap-allocated types (String, Vec, Box): assignment moves. Clone: explicit deep copy. let v2 = v.clone(); -- both v and v2 are valid, independent copies. Clone is explicit (no hidden copies) and potentially expensive (the programmer sees the cost). Interview question: "Why does Rust have ownership instead of garbage collection?" Answer: zero-cost memory safety. No runtime GC pauses, deterministic deallocation (drops happen at known points), and the compiler catches memory bugs. Tradeoff: harder to write (fighting the borrow checker), but bugs found at compile time instead of production.

## Lifetimes

Lifetimes ensure references do not outlive the data they point to. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str -- the returned reference lives at most as long as the shorter-lived input. The compiler uses lifetimes to verify: no dangling references, no use-after-free, and no returning references to local variables. Lifetime elision: in most cases, the compiler infers lifetimes automatically. Rules: (1) Each input reference gets its own lifetime. (2) If there is one input lifetime, the output gets the same. (3) If one input is &self, the output gets the lifetime of self. When you must annotate: when the compiler cannot infer the relationship (multiple inputs with different lifetimes and an output reference). struct with references: struct Excerpt<'a> { text: &'a str }. The struct cannot outlive the data it references. The lifetime parameter makes this explicit. static lifetime: &'static str lives for the entire program (string literals, leaked memory). Use sparingly -- most data should have bounded lifetimes. Interview question: "What is a lifetime in Rust and why is it needed?" Answer: a lifetime is a compile-time annotation that tells the compiler how long a reference is valid. Without lifetimes, the compiler cannot verify that references do not dangle. Lifetimes enable: returning references from functions, storing references in structs, and safe multithreaded reference sharing -- all verified at compile time with no runtime cost.

## Traits and Generics

Traits define shared behavior (similar to interfaces in Go/Java): trait Summary { fn summarize(&self) -> String; }. Types implement traits: impl Summary for Article { fn summarize(&self) -> String { ... } }. Trait bounds: fn print_summary(item: &impl Summary) or fn print_summary<T: Summary>(item: &T). Only types implementing Summary can be passed. Multiple bounds: T: Summary + Display. Standard traits: (1) Clone -- explicit deep copy. (2) Copy -- implicit bitwise copy (for simple types). (3) Debug -- debug formatting {:?}. (4) Display -- user-facing formatting {}. (5) Iterator -- for-loop support. (6) From/Into -- type conversion. (7) Drop -- custom cleanup on deallocation (like a destructor). Trait objects: dyn Summary -- dynamic dispatch (vtable lookup at runtime). Box<dyn Summary> is a trait object on the heap. Use when: the concrete type is not known at compile time (heterogeneous collections, plugin architectures). Static dispatch (generics): the compiler generates specialized code for each concrete type. Zero overhead (no vtable lookup). Use for: performance-critical code where types are known at compile time. Derive macros: #[derive(Debug, Clone, PartialEq)] auto-generates trait implementations. Works for: standard traits on structs where the default implementation makes sense. Interview question: "What is the difference between impl Trait and dyn Trait?" Answer: impl Trait is static dispatch (monomorphized at compile time, zero overhead, but the type is fixed). dyn Trait is dynamic dispatch (vtable at runtime, slight overhead, but supports heterogeneous types).

## Error Handling: Result and Option

Rust uses Result<T, E> for fallible operations and Option<T> for nullable values. No null pointers. No exceptions. Result<T, E>: either Ok(value) or Err(error). The caller MUST handle both cases (the compiler enforces this). The ? operator propagates errors: let data = fs::read_to_string("file.txt")?; -- if Err, return the error immediately. If Ok, unwrap the value. Chaining: file.read_to_string(&mut buf)?; parse(&buf)?; validate(&parsed)?;. Each ? propagates errors up the call stack. Custom errors: enum AppError { NotFound(String), Database(sqlx::Error), Validation(String) }. Implement std::error::Error and Display. Use thiserror crate for derive macros. anyhow crate: for application code that does not need to match on specific error types. anyhow::Result<T> wraps any error. Context: .context("failed to read config")?. Good for: application code, scripts, and CLIs. Option<T>: either Some(value) or None. Forces the caller to handle the absence case. let user = find_user(id)?; -- if None, propagate. if let Some(user) = find_user(id) { ... }. map, and_then, unwrap_or_default for functional chaining. Interview question: "How does Rust error handling compare to exceptions?" Answer: Rust errors are values (Result/Option), not control flow (try/catch). Benefits: errors are visible in the type signature, cannot be accidentally ignored (must handle Result), and have zero runtime overhead (no stack unwinding). Tradeoff: more verbose (? helps but every fallible call is explicit).

## Async/Await and Concurrency

Async Rust: async fn fetch_data() -> Result<Data> { let res = client.get(url).send().await?; res.json().await }. Async functions return a Future. .await suspends until the Future completes. The runtime (tokio or async-std) drives Futures to completion. tokio: the standard async runtime for Rust. Provides: task spawning (tokio::spawn), timers (tokio::time::sleep), I/O (tokio::net, tokio::fs), channels (tokio::sync::mpsc), and synchronization (tokio::sync::Mutex). Concurrency without data races: Rust ownership + Send/Sync traits. Send: a type can be transferred to another thread. Sync: a type can be referenced from multiple threads. The compiler enforces these bounds. If a type is not Send, you cannot spawn it in a tokio task. This catches data races at compile time. Arc<T> (Atomic Reference Counting): shared ownership across threads. Arc<Mutex<T>>: shared mutable state. The Mutex ensures exclusive access. Arc enables multiple owners. This is the safe equivalent of sharing a pointer in C++. Channels: tokio::sync::mpsc (multi-producer, single-consumer). Similar to Go channels. Send messages between tasks without shared state. Interview question: "How does Rust prevent data races?" Answer: ownership (one owner), borrowing rules (exclusive mutable access), Send/Sync traits (compile-time thread safety verification), and no shared mutable state without explicit synchronization (Mutex, channels). Data races are impossible in safe Rust -- they are compile errors, not runtime bugs.
