Rust Interview Questions
These are questions that come up again and again in Rust interviews, from junior roles to senior ones. Each answer is short enough to say out loud, and links to the lesson that explains the topic in depth.
Don’t memorize these word for word. Interviewers can tell, and they usually ask a follow-up question to see if you really understand. Use this page to check what you know, then go to the linked lesson for anything that feels shaky. If you can explain an answer in your own words and write a tiny example, you’re ready.
Basics
Section titled “Basics”Q: What is Cargo?
Section titled “Q: What is Cargo?”Cargo is Rust’s official build tool and package manager. It comes with Rust. With one tool you can:
- create a project (
cargo new), - build and run it (
cargo build,cargo run), - run tests (
cargo test), - add libraries, called crates, from crates.io (
cargo add serde).
A project’s settings and dependencies live in Cargo.toml. Exact versions are locked in Cargo.lock, so builds are repeatable. A good extra point: other languages often need several tools for this, but in Rust everyone uses Cargo, so every project looks familiar.
Learn more: Cargo and Crates, Get Started.
Q: What’s the difference between shadowing and mut?
Section titled “Q: What’s the difference between shadowing and mut?”mut lets you change the value of the same variable. The type must stay the same.
Shadowing means writing let again with the same name. It creates a new variable that hides the old one. The new one can even have a different type.
fn main() { let spaces = " "; let spaces = spaces.len(); // shadowing: a new variable, now a number println!("{spaces}");
let mut count = 1; count += 1; // mut: the same variable gets a new value println!("{count}");}Output:
32Shadowing is handy for transforming a value step by step (like parsing text into a number) without inventing names like input_str and input_num. And the final variable is still immutable.
Learn more: Variables.
Q: What’s the difference between a macro and a function?
Section titled “Q: What’s the difference between a macro and a function?”A function is called at runtime with a fixed number of typed arguments. A macro runs at compile time: it takes code and writes more code before the compiler checks it. You can spot macros by the !, like println!, vec!, and format!.
Macros can do things functions can’t, like take a variable number of arguments (println!("{} {}", a, b)) or check a format string at compile time. Rust macros also work on the code’s structure, not raw text, so they avoid the classic C macro bugs:
macro_rules! square { ($x:expr) => { $x * $x };}
fn main() { // In C, a text-based macro would give 2 + 1 * 2 + 1 = 5. println!("{}", square!(2 + 1));}Output:
9The honest trade-off: macros are harder to read, write, and debug. Use a function when a function can do the job.
Learn more: Macros.
Q: What does “zero-cost abstractions” mean?
Section titled “Q: What does “zero-cost abstractions” mean?”It means high-level features shouldn’t make your program slower than the low-level code you’d write by hand. “You don’t pay for what you don’t use, and what you do use, you couldn’t write better yourself.”
For example, an iterator chain like .iter().filter(...).map(...).sum() usually compiles to the same machine code as a hand-written loop. Generics are another example: the compiler makes a specialized copy for each type you use (called monomorphization), so there’s no lookup at runtime.
Be honest about the “cost” part: it’s zero runtime cost. You still pay in compile time, and generics can make the program file bigger.
Learn more: Iterators, Generics.
Q: Are Rust iterators lazy?
Section titled “Q: Are Rust iterators lazy?”Yes. Methods like .map() and .filter() don’t do any work on their own. They build a description of the work. Nothing happens until something consumes the iterator, like .collect(), .sum(), or a for loop.
fn main() { let numbers = vec![1, 2, 3]; let doubled = numbers.iter().map(|n| { println!("doubling {n}"); n * 2 });
println!("Nothing has run yet."); let result: Vec<i32> = doubled.collect(); println!("{:?}", result);}Output:
Nothing has run yet.doubling 1doubling 2doubling 3[2, 4, 6]That’s also why the compiler warns you if you write numbers.iter().map(...); and never use the result: it would do nothing.
Learn more: Iterators.
Ownership & Borrowing
Section titled “Ownership & Borrowing”Q: What are the ownership rules?
Section titled “Q: What are the ownership rules?”There are three:
- Each value in Rust has one owner (a variable).
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (its memory is freed).
These rules let Rust free memory at exactly the right time without a garbage collector, and without you calling free by hand. The compiler checks them, so mistakes like “use after free” or “double free” can’t happen in safe code.
Q: What’s the difference between move, copy, and clone?
Section titled “Q: What’s the difference between move, copy, and clone?”- Move: assigning or passing a value transfers ownership. The old variable can’t be used anymore. This is the default for types like
StringandVec. Nothing is copied except a small pointer, length, and capacity. - Copy: for small, simple types like
i32,f64,bool, andchar, assignment makes a cheap bit-for-bit copy, and both variables stay usable. - Clone:
.clone()is an explicit deep copy. For aString, it copies all the text into new memory. It can be expensive, which is why Rust makes you write it.
fn main() { let x = 5; let y = x; // Copy: x is still usable println!("{x} {y}");
let a = String::from("hi"); let b = a.clone(); // Clone: a full copy of the text let c = a; // Move: `a` can't be used after this line println!("{b} {c}");}Output:
5 5hi hiLearn more: Ownership.
Q: What is the Copy trait, and which types have it?
Section titled “Q: What is the Copy trait, and which types have it?”Copy is a marker trait that says “this type can be duplicated just by copying its bits”. Types that are Copy are copied instead of moved on assignment.
Integers, floats, bool, char, shared references (&T), and tuples or arrays made only of Copy types are all Copy. String, Vec, and Box are not, because they own memory somewhere else, and copying the pointer would give two owners.
You can derive it for your own types, but only if every field is Copy:
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Copy always requires Clone too. Also, a type that implements Drop can’t be Copy.
Learn more: Common Traits, Ownership.
Q: What’s the difference between String and &str?
Section titled “Q: What’s the difference between String and &str?”Stringis an owned, growable piece of text stored on the heap. You can change it, for example withpush_str.&str(a “string slice”) is a borrowed view into some text. It’s just a pointer and a length. String literals like"hello"are&str.
A good rule: functions that only read text should take &str. That way they accept both, because a &String turns into a &str automatically.
fn greet(name: &str) { println!("Hello, {name}!");}
fn main() { let owned: String = String::from("Ana"); let literal: &str = "Ben";
greet(&owned); // &String works where &str is expected greet(literal);
let mut full = owned; full.push_str(" Smith"); // only a String can grow greet(&full);}Output:
Hello, Ana!Hello, Ben!Hello, Ana Smith!Q: What are the borrowing rules?
Section titled “Q: What are the borrowing rules?”At any moment, for a given value, you can have either:
- any number of shared references (
&T), or - exactly one mutable reference (
&mut T),
but not both. And every reference must always point to valid data.
Why? If someone could change data while someone else is reading it, you’d get bugs like a vector moving its memory while you hold a pointer into it. In threads, it’s exactly what a data race is. Rust prevents it at compile time. Since the 2018 edition, the compiler is smart about when a borrow ends (this is called non-lexical lifetimes): a borrow only lasts until its last use, not until the end of the block.
Learn more: Borrowing.
Q: What are lifetimes?
Section titled “Q: What are lifetimes?”A lifetime is the compiler’s name for “how long a reference is valid”. Every reference has one. Most of the time the compiler figures them out on its own (this is called lifetime elision), so you never see them.
You only write them when the compiler can’t tell how the output reference relates to the inputs, like a function that returns one of two borrowed strings:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b }}
fn main() { let first = String::from("borrow checker"); let second = String::from("lifetimes"); println!("{}", longest(&first, &second));}Output:
borrow checkerAn important point to say in an interview: lifetime annotations don’t change how long anything lives. They only describe relationships so the compiler can check that no reference outlives the data it points to.
Learn more: Lifetimes.
Types & Traits
Section titled “Types & Traits”Q: Rust has no null. What does it use instead?
Section titled “Q: Rust has no null. What does it use instead?”Option<T>, an enum with two variants: Some(value) or None. Because it’s a different type from T, the compiler forces you to handle the None case before you can use the value. You can’t forget a null check, because the code won’t compile.
fn find_user(id: u32) -> Option<&'static str> { match id { 1 => Some("Ana"), 2 => Some("Ben"), _ => None, }}
fn main() { for id in [1, 3] { match find_user(id) { Some(name) => println!("Found {name}"), None => println!("No user with id {id}"), } }}Output:
Found AnaNo user with id 3A nice extra detail: Option<&T> and Option<Box<T>> take no extra memory compared to a plain pointer. Rust uses the “null” bit pattern internally to mean None, so safety costs nothing here.
Q: What is a trait? Is it the same as an interface?
Section titled “Q: What is a trait? Is it the same as an interface?”A trait defines a set of methods that a type can implement. It’s similar to an interface in Java, C#, or Go, and that’s a fine way to start your answer. Then mention the differences:
- Traits can have default method implementations.
- You can implement a trait for a type after the type was written, even for types you didn’t write, like implementing your own trait for
i32. (The “orphan rule” says either the trait or the type must be yours.) - Traits are used as bounds on generics (
T: Display), which is resolved at compile time, and also as trait objects (dyn Display) at runtime. - Rust has no class inheritance. Traits plus composition replace it.
Learn more: Traits, Common Traits.
Q: What’s the difference between generics and trait objects?
Section titled “Q: What’s the difference between generics and trait objects?”Both let one function work with many types. The difference is when the type is decided:
- Generics (
fn f<T: Speak>(x: &T)) use static dispatch. The compiler makes a separate copy of the function for each type. Calls are as fast as normal function calls, but the program can get bigger. - Trait objects (
&dyn Speak,Box<dyn Speak>) use dynamic dispatch. One copy of the function, and the right method is looked up at runtime through a table (a “vtable”). It’s a tiny bit slower, but it lets you mix different types in one collection.
trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Woof") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Meow") }}
fn speak_static<T: Speak>(animal: &T) -> String { animal.speak()}
fn speak_dynamic(animal: &dyn Speak) -> String { animal.speak()}
fn main() { println!("{}", speak_static(&Dog)); println!("{}", speak_dynamic(&Cat));
// Only trait objects can mix types in one Vec: let animals: Vec<Box<dyn Speak>> = vec![Box::new(Dog), Box::new(Cat)]; for animal in &animals { println!("{}", animal.speak()); }}Output:
WoofMeowWoofMeowRule of thumb: start with generics. Use trait objects when you need a collection of different types, or want to keep compile times and binary size down.
Learn more: Generics, Trait Objects.
Q: What’s the difference between Box<dyn Trait> and impl Trait?
Section titled “Q: What’s the difference between Box<dyn Trait> and impl Trait?”Both let you say “some type that implements this trait” without naming it. The difference is how many concrete types are allowed:
impl Traitmeans one specific type that the compiler knows but doesn’t make you write. It’s static dispatch: no heap allocation, no vtable, as fast as naming the type yourself. In argument position (fn f(x: impl Display)) it’s just a shorter way to write a generic. In return position it’s how you return things with long or unnameable types, like closures and iterator chains.Box<dyn Trait>is a trait object on the heap. The actual type can be different each time, decided at runtime, and the method is found through a vtable. That costs an allocation and an indirect call, but it’s the only way to return one of several types, or store them in oneVec.
use std::fmt::Display;
// impl Trait: one hidden type, fixed at compile time.fn evens(limit: u32) -> impl Iterator<Item = u32> { (0..limit).filter(|n| n % 2 == 0)}
// Box<dyn Trait>: the real type can change at runtime.fn label(as_number: bool) -> Box<dyn Display> { if as_number { Box::new(42) } else { Box::new("forty-two") }}
fn main() { let v: Vec<u32> = evens(10).collect(); println!("{v:?}"); println!("{}", label(true)); println!("{}", label(false));}Output:
[0, 2, 4, 6, 8]42forty-twoA good detail to mention: if you change label to return impl Display and remove the boxes, it won’t compile. The if returns an i32 and the else returns a &str, and impl Trait must be one single type. Rule of thumb: use impl Trait by default, and switch to Box<dyn Trait> when you truly need to choose between types at runtime.
Learn more: Trait Objects, Generics.
Q: Why does match have to be exhaustive?
Section titled “Q: Why does match have to be exhaustive?”A match must handle every possible value, or the code won’t compile. This means that when you add a new variant to an enum, the compiler shows you every place that needs updating. In other languages, a forgotten case in a switch silently does nothing.
You can use _ as a catch-all, but be careful: a catch-all also hides new variants you add later, so you lose that safety net.
Learn more: Match, Pattern Matching, Enums.
Error Handling
Section titled “Error Handling”Q: How does Rust handle errors without exceptions?
Section titled “Q: How does Rust handle errors without exceptions?”Recoverable errors are normal values of type Result<T, E>: either Ok(value) or Err(error). A function that can fail says so in its return type, and the caller must deal with it. Nothing jumps invisibly up the stack like an exception does.
The benefits: you can see from a function’s signature that it might fail, and the compiler warns you if you ignore a Result. The cost is a bit more typing, which the ? operator mostly removes.
Learn more: Result.
Q: When should you use panic! instead of returning a Result?
Section titled “Q: When should you use panic! instead of returning a Result?”- Return a
Resultwhen failure is expected and the caller could reasonably handle it: a missing file, bad user input, a network timeout. - Panic when something is a bug, meaning the program reached a state that “should be impossible”, and continuing would be wrong. Indexing past the end of a vector panics for this reason.
Panics are also fine in tests, examples, and quick prototypes. In a library, avoid panicking on input the caller controls. Return an error and let them decide.
Q: What’s the difference between unwrap, expect, and ??
Section titled “Q: What’s the difference between unwrap, expect, and ??”unwrap()gives you the value insideOk/Some, or panics with a generic message.expect("msg")does the same, but panics with your message. Use it to explain why you believe it can’t fail.?doesn’t panic. OnErr/None, it returns the error from the current function, so the caller can handle it. The function must return aResultorOptionitself.
use std::num::ParseIntError;
fn double(text: &str) -> Result<i32, ParseIntError> { let n: i32 = text.parse()?; // on error, return it to the caller Ok(n * 2)}
fn main() { println!("{:?}", double("21")); println!("{:?}", double("abc"));
let n: i32 = "7".parse().expect("a hard-coded number should always parse"); println!("{n}");}Output:
Ok(42)Err(ParseIntError { kind: InvalidDigit })7In production code, prefer ?. Use expect when failure would be a bug, and keep unwrap for tests and quick experiments.
Q: How do you create your own error types?
Section titled “Q: How do you create your own error types?”Usually with an enum that lists the ways your code can fail, plus implementations of std::fmt::Display and std::error::Error. Implementing From for other error types lets ? convert them automatically.
In practice, many projects use helper crates: thiserror to define error types in libraries, and anyhow for applications that just need to pass errors up with context. For quick programs, Box<dyn Error> can hold any error.
Learn more: Custom Errors.
Q: When would you use anyhow vs thiserror?
Section titled “Q: When would you use anyhow vs thiserror?”They solve two halves of the same problem, so the honest answer is “often both”:
thiserroris for libraries, or any code whose callers need to react to specific errors. You write your own error enum and#[derive(Error)]fills in theDisplayandErrorboilerplate. Callers get a real type they canmatchon, likeConfigError::MissingField.anyhowis for applications, where most errors just need to be reported, not handled one by one. Every function returnsanyhow::Result<T>,?accepts almost any error type, and.context("while reading config")adds a human-friendly explanation on top. The trade-off: callers can’t easilymatchon the error anymore.
A common setup is thiserror inside library crates and anyhow in main and the rest of the binary. Neither crate is magic. Both only remove boilerplate on top of the standard std::error::Error trait, so you can always write the same thing by hand.
Learn more: anyhow and thiserror, Custom Errors.
Memory & Smart Pointers
Section titled “Memory & Smart Pointers”Q: Does Rust have a garbage collector?
Section titled “Q: Does Rust have a garbage collector?”No. Memory is freed when its owner goes out of scope, at a point the compiler knows ahead of time (this is called RAII, and the cleanup method is Drop). That gives you predictable performance with no GC pauses.
When you really need shared ownership, you opt in with reference counting (Rc or Arc), which frees the value when the last owner is gone.
Learn more: Ownership, Smart Pointers.
Q: When would you use Box, Rc, Arc, and RefCell?
Section titled “Q: When would you use Box, Rc, Arc, and RefCell?”Box<T>: one owner, value on the heap. Use it for recursive types (like a tree or linked list), large values, or trait objects (Box<dyn Trait>).Rc<T>: several owners in one thread. It counts references and frees the value when the count reaches zero. The data is read-only.Arc<T>: likeRc, but the count is atomic, so it’s safe to share across threads. Slightly slower thanRc.RefCell<T>: lets you change data through a shared reference (“interior mutability”). The borrow rules are checked at runtime instead of compile time, and breaking them panics.
They combine. Rc<RefCell<T>> is “shared and changeable, in one thread”. Arc<Mutex<T>> is the thread-safe version.
use std::cell::RefCell;use std::rc::Rc;
fn main() { let shared = Rc::new(RefCell::new(vec![1, 2])); let other_owner = Rc::clone(&shared);
other_owner.borrow_mut().push(3);
println!("{:?}", shared.borrow()); println!("owners: {}", Rc::strong_count(&shared));}Output:
[1, 2, 3]owners: 2Learn more: Smart Pointers.
Q: Can you leak memory in safe Rust?
Section titled “Q: Can you leak memory in safe Rust?”Yes, and it’s honest to say so. Rust guarantees memory safety (no use-after-free, no dangling pointers, no data races), but leaking memory is not considered unsafe. Two common ways:
- An
Rccycle: two values that point to each other withRcnever reach a count of zero. The fix is to make one direction aWeakreference. - Functions like
Box::leakandstd::mem::forget, which leak on purpose.
In everyday code, leaks are rare, because ownership frees almost everything automatically.
Learn more: Smart Pointers.
Q: What is unsafe, and when is it used?
Section titled “Q: What is unsafe, and when is it used?”unsafe marks a block (or function) where you can do five extra things the compiler can’t check, such as dereferencing raw pointers, calling unsafe functions (including C code through FFI), or accessing mutable statics. Inside it, you promise the rules are upheld.
Important: unsafe doesn’t turn off the borrow checker or other checks. It only unlocks those few extra operations.
It’s used for talking to C libraries, writing low-level data structures, and squeezing out performance where the compiler can’t prove something is safe. The standard library itself uses unsafe inside safe APIs like Vec. Good practice: keep unsafe blocks small, wrap them in a safe API, and write a // SAFETY: comment explaining why it’s correct. Most application code never needs it.
Learn more: Unsafe Rust.
Concurrency & Async
Section titled “Concurrency & Async”Q: What do Send and Sync mean?
Section titled “Q: What do Send and Sync mean?”They’re marker traits the compiler uses to keep threads safe:
Send: a value of this type can be moved to another thread.Sync: a value of this type can be shared by reference between threads (&TisSend).
Most types are both, and the compiler implements them automatically. The famous exception is Rc, whose count isn’t thread-safe. Try to send one to a thread, and it won’t compile:
use std::rc::Rc;use std::thread;
fn main() { let data = Rc::new(5); let handle = thread::spawn(move || { println!("{data}"); }); handle.join().unwrap();}error[E0277]: `Rc<i32>` cannot be sent between threads safely ... = help: within `{closure@src/main.rs:6:32: 6:39}`, the trait `Send` is not implemented for `Rc<i32>`Switch Rc to Arc and it works. This is a big part of what people mean by Rust’s “fearless concurrency”: thread-safety mistakes are compile errors.
Learn more: Concurrency.
Q: How do you share changeable data between threads?
Section titled “Q: How do you share changeable data between threads?”The classic answer is Arc<Mutex<T>>. Arc lets several threads own the value. Mutex makes sure only one thread at a time can change it: you call .lock(), and you get a guard that unlocks automatically when it goes out of scope.
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = Vec::new();
for _ in 0..4 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { for _ in 0..1000 { *counter.lock().unwrap() += 1; } })); }
for handle in handles { handle.join().unwrap(); }
println!("Total: {}", *counter.lock().unwrap());}Output:
Total: 4000Also mention the alternative: instead of sharing memory, send messages between threads with channels (std::sync::mpsc). And for simple counters, atomic types like AtomicUsize avoid a lock.
Learn more: Concurrency.
Q: How do channels compare to shared state with Mutex?
Section titled “Q: How do channels compare to shared state with Mutex?”They are two styles of sharing work between threads:
- Channels (
std::sync::mpsc) pass messages. A value is moved into the channel and comes out on the other side, so only one thread owns it at a time. They fit pipelines and worker pools: “workers produce results, one thread collects them.” They’re usually easier to reason about, because no two threads touch the same data. - Shared state (
Arc<Mutex<T>>) means several threads use the same data, taking turns through a lock. It fits when many threads need to read and update one structure, like a cache or a table of counters. The risks are deadlocks, and slowdowns when threads keep waiting for the lock.
Here’s the counter from the previous answer, rewritten with a channel. Each worker counts on its own, with no lock, and sends its result:
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
for _ in 0..4 { let tx = tx.clone(); thread::spawn(move || { let mut count = 0; for _ in 0..1000 { count += 1; } tx.send(count).unwrap(); }); } drop(tx); // drop the original sender, or rx.iter() never ends
let total: i32 = rx.iter().sum(); println!("Total: {total}");}Output:
Total: 4000The drop(tx) line is a great detail to mention: rx.iter() keeps waiting until every sender is gone, so forgetting it makes the program hang. In both styles, Rust checks the thread safety at compile time. The choice is about which design is clearer for your problem, and many programs use both.
Learn more: Channels, Concurrency.
Q: Does Rust prevent all concurrency bugs?
Section titled “Q: Does Rust prevent all concurrency bugs?”No, and saying so shows real understanding. Safe Rust prevents data races (two threads touching the same memory at once, with at least one writing). It does not prevent:
- Deadlocks, for example two threads each waiting for a lock the other holds.
- Race conditions in your logic, for example checking a value and then acting on it after another thread has changed it.
So Rust removes a whole class of hard bugs, but you still need to design your locking carefully.
Learn more: Concurrency.
Q: What’s the difference between async and threads?
Section titled “Q: What’s the difference between async and threads?”- Threads are run by the operating system. Each has its own stack, and the OS switches between them. They’re great for CPU-heavy work. But thousands of threads use a lot of memory.
- Async tasks are much lighter. An
async fnreturns aFuture, and a runtime (usually Tokio) runs many futures on a few threads. When a task is waiting (for the network, a timer, a file),.awaitlets other tasks run. It’s ideal for programs that wait a lot, like web servers handling thousands of connections.
use tokio::time::{sleep, Duration};
async fn fetch(name: &str, ms: u64) -> String { sleep(Duration::from_millis(ms)).await; // pretend to wait for the network format!("{name} finished")}
#[tokio::main]async fn main() { // Both run at the same time, so this takes about 200ms, not 300ms. let (a, b) = tokio::join!(fetch("first", 200), fetch("second", 100)); println!("{a}"); println!("{b}");}Output:
first finishedsecond finishedTwo honest details interviewers like: Rust futures are lazy and do nothing until awaited, and the standard library doesn’t include a runtime, so you choose one. Also, blocking code (like a long calculation or std::thread::sleep) inside an async task blocks the other tasks on that thread.
Learn more: Async/Await, Concurrency.
Tips for answering in an interview
Section titled “Tips for answering in an interview”- Start simple, then go deeper. Give a one-sentence answer first (“
Rcis a reference-counted pointer for shared ownership in one thread”). Then add details if they seem interested. - Explain the why. Rust’s rules exist to prevent specific bugs. Saying “the borrow rules prevent data races and dangling pointers” is stronger than just reciting the rules.
- Use a tiny example. Writing three lines of code, or describing one, often explains more than a long speech.
- Be honest about trade-offs. Rust has costs: slower compile times, a learning curve, more typing for error handling. Admitting them shows experience, not weakness.
- Say “I don’t know” when you don’t, then reason out loud. “I haven’t used
Pindirectly, but I know it’s about futures that must not move in memory” is a great answer. - Have a story ready. Interviewers often ask, “Tell me about a time the borrow checker stopped you.” Think of one real example from your own code, and what you learned from it.
- Practice by writing code. The Exercises page and the Guessing Game and To-Do List projects are good warm-ups.
Quick check
Section titled “Quick check”Quick check
1. Which smart pointer would you use to share read-only data between threads?
2. What does the ? operator do when it meets an Err?
3. Which of these does safe Rust prevent at compile time?
4. Do lifetime annotations change how long a value lives?
Score: 0 / 4
Read more
Section titled “Read more”- The Rust Programming Language (the Rust Book): the official book. Most answers on this page come straight from its chapters on ownership, traits, errors, and concurrency.
- Rust by Example: short runnable examples, handy for refreshing the syntax right before an interview.
- The Rust Reference: the precise rules of the language, for when an interviewer asks “but exactly when does that happen?”
- The Rustonomicon: the guide to
unsafeRust, with deeper background onSend,Sync, and memory layout for senior-level questions. - Asynchronous Programming in Rust: how futures,
async/.await, and runtimes fit together, in the official async book from the Rust project.