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.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How does Rust ownership prevent memory bugs without garbage collection?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Three ownership rules enforced at compile time: (1) Each value has exactly one owner. (2) When the owner goes out of scope, the value is freed (dropped). (3) Ownership can be transferred (moved): let s2 = s1 makes s1 invalid. These rules eliminate use-after-free, double-free, and memory leaks with zero runtime cost (no GC pauses, deterministic deallocation). Borrowing extends this: immutable borrows (&T) allow multiple readers. Mutable borrows (&mut T) give exclusive access. The borrow checker enforces: EITHER multiple immutable references OR one mutable reference (not both). This prevents data races at compile time. Tradeoff: harder to write (fighting the borrow checker) but bugs are found at compile time instead of production. No runtime overhead — the checks happen entirely at compilation.”}},{“@type”:”Question”,”name”:”How does Rust prevent data races at compile time?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Rust prevents data races through multiple compile-time mechanisms: (1) Ownership — only one owner can mutate data. Moving ownership transfers exclusive access. (2) Borrowing rules — either multiple readers (&T) or one writer (&mut T), never both simultaneously. (3) Send/Sync traits — Send means a type can be transferred to another thread. Sync means it can be referenced from multiple threads. The compiler enforces these bounds. If a type is not Send, you cannot spawn it in a thread. (4) No shared mutable state without explicit synchronization: Arc<Mutex> for shared mutable data (Mutex ensures exclusive access, Arc enables multiple owners across threads). Channels (tokio::sync::mpsc) for message-passing between tasks. The result: data races are compile errors in Rust, not runtime bugs. This is fearless concurrency — concurrent code that the compiler guarantees is free from data races.”}}]}