Rust loops have a shape. The borrow checker fights come first, then a couple of trait-dispatch questions, then someone hands you an Arc<Mutex<T>> and asks why the naive version does not compile. Interviewers grade whether you can name the mechanism behind the rule, not whether you can recite the rule. This list comes from the rubric behind our Rust modules and real loops.
At a glance, what each area tends to ask:
| Area | Band | Typical question | Study |
|---|---|---|---|
| ownership | junior/middle | move vs Copy, the borrow rule, borrow-checker fights | Ownership & borrowing |
| traits | middle/senior | generics vs dyn, Option/Result, the ? operator | Types & traits |
| concurrency | middle/senior | Send/Sync, Arc<Mutex<T>>, guards across .await | Rust concurrency |
| theory | any | data race vs deadlock, what Rust does not prevent | Concurrency |
Ownership: the part that fails candidates first
These decide whether the conversation continues. Answer them without pausing.
What happens after let s2 = s1;?
If s1 is a String, ownership moves to s2 and s1 is invalidated. Touching s1 afterward is a compile error, not a runtime one. If s1 were an i32, it would copy instead, because fixed-size stack types are Copy and the original stays valid.
Follow-up "Why does String move but i32 copy?" String owns a heap buffer. A bitwise duplicate would leave two owners of one allocation and a double free at drop, so String implements Clone but not Copy.
Fix this borrow-checker fight
The classic plant is mutation while iterating:
for x in &vec {
vec.push(x * 2); // WRONG: the loop holds a shared borrow of vec
}
// RIGHT: collect what you need first, then mutate
let doubled: Vec<_> = vec.iter().map(|x| x * 2).collect();
vec.extend(doubled);
Trap the reflex fix is .clone() to silence the compiler. That works and marks you as junior. The senior move is to restructure so a single borrow covers the read, because push can reallocate the Vec and dangle any live reference into it.
The shared-XOR-mutable rule exists for more than safety. A &mut T is effectively noalias, which lets the optimizer assume nothing else touches the data. That is why it holds even in single-threaded code with no data-race risk.
Study Rust ownership & borrowing
Traits and dispatch
Vec<T> or Vec<Box<dyn Draw>>?
Use Vec<T> when every element is the same concrete type. Reach for Vec<Box<dyn Draw>> when the collection is heterogeneous, since you cannot store two different concrete types in one Vec<T>. The tradeoff is dispatch: generics get monomorphized into a specialized copy per type (inlinable, zero runtime cost, more code), while dyn Trait is a fat pointer (data plus vtable) and each call is one indirect load that cannot inline.
Follow-up "When can't a trait be used as dyn?" Dyn compatibility (renamed from "object safety" in 2024) blocks it when a method returns Self, takes generic type parameters, or requires Self: Sized. A vtable has one slot per method and cannot encode an unknown Self size or an unmonomorphized generic.
Why is unwrap a red flag?
// WRONG: panics the request thread on any malformed input
let port: u16 = config.get("port").unwrap().parse().unwrap();
// RIGHT: propagate with ?, converting the error via From
let port: u16 = config.get("port").ok_or(Error::MissingPort)?.parse()?;
The ? operator early-returns the Err and runs an implicit From::from to convert it into the function's declared error type. unwrap on external input turns a recoverable error into a panic. It is legitimate for an invariant you just established (a regex literal that always compiles, a slice you bounds-checked) and in tests, where expect("reason") documents that invariant. On a request path it is the graded red flag.
Study Rust types & traits
Concurrency: where seniors get separated
Send vs Sync, in one line each
Send means a value is safe to move to another thread. Sync means it is safe to share by reference across threads. The formal relation: T: Sync if and only if &T: Send. Both are auto traits the compiler derives structurally, so you almost never implement them by hand.
Follow-up "Why can't Rc cross a thread boundary?" Its reference count is a plain non-atomic integer, so two threads touching the count would race it. Rc is !Send and !Sync, and the compiler stops you. Arc uses atomic increments, so it is Send + Sync when its contents are.
Share mutable state between threads
Arc alone gives shared ownership but only immutable access. To mutate, wrap the inner value:
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { // move is required: spawn needs 'static
*c.lock().unwrap() += 1; // lock() returns Err if a holder panicked (poisoning)
});
Trap dropping the move keyword. thread::spawn requires the closure to be Send + 'static, so it cannot borrow a stack local. If you genuinely need to borrow locals, thread::scope (stable since 1.63) joins every thread before returning, which makes the borrow sound.
The async trap: a guard held across .await
// WRONG: std MutexGuard is !Send, so this future is !Send and won't compile under tokio::spawn
let guard = data.lock().unwrap();
do_async_work().await; // guard still alive across the yield point
An .await is a suspension point where the task can be parked and its worker thread reused. A std::sync::MutexGuard is !Send, so a future holding one across .await becomes !Send and fails to compile under tokio::spawn. Even where it compiles on a current-thread runtime, holding a blocking lock across a yield can stall the worker. Prefer std::sync::Mutex and release the guard before awaiting. Reach for tokio::sync::Mutex only when the lock must survive an .await, since its lock().await yields on contention and costs more.
Follow-up "What about a CPU-heavy loop inside an async task?" A tokio worker runs many tasks cooperatively, so a non-yielding call starves every other task on that thread. Move it to tokio::task::spawn_blocking.
Study Rust concurrency
The theory question underneath
"Fearless concurrency" is easy to overclaim. Rust prevents data races, a memory-safety property: unsynchronized concurrent access with at least one writer becomes a compile error. It does not prevent deadlocks, livelock, or logical races on external state. Lock two mutexes in opposite orders across two threads and you still deadlock. That code compiles fine.
Interviewers probe this boundary because candidates who can name what Rust does not save them from have usually shipped concurrent Rust. The deadlock theory itself (the four Coffman conditions, mutex vs semaphore, optimistic vs pessimistic locking) sits one layer down and shows up in backend loops regardless of language.
Study Concurrency fundamentals. When the question grows into "design a rate limiter that holds up at 10x," it has become a system design round, where the locking model is one component rather than the whole answer.
What the grader listens for
We grade against a written rubric that weighs mechanisms over vocabulary. Saying "aliasing rule" earns nothing on its own. Showing that a push reallocated the Vec and dangled a live reference earns it.
Red flags cloning to silence the borrow checker without noticing the borrow was the point; claiming
Arcalone makes data mutable across threads; treatingunwrapon request input as fine; and asserting Rust prevents all race conditions. Any of these caps the score no matter how fluent the rest of the answer sounds.
The free notes run from the junior floor to the senior signals. Start with ownership; the diagnostic will tell you which band you clear today.