1. What is the fundamental rule of variable mutability in Rust by default?
Answer: Variables are immutable by default and must be explicitly declared with mut.
In Rust, let x = 5; creates a variable you cannot reassign:
let x = 5;
x = 6; // compile error: cannot assign to immutable variable
let mut y = 5;
y = 6; // OK — y is mutable
This immutability-by-default is a safety design: it removes an entire class of accidental-mutation bugs at compile time, and it lets the compiler reason safely about aliasing and concurrency. If you need to change a binding, you opt in with mut. (Note: mut controls the binding — the variable’s ability to be reassigned — separate from the mutability of the data a reference points to.)
The interview answer: variables are immutable by default; mut explicitly opts into mutability.
2. Which rule best describes Rust’s borrowing rules for references to a resource at any given time?
Answer: Either one mutable reference OR any number of immutable references — never both.
Rust’s aliasing rule, enforced entirely at compile time:
- Any number of immutable references (
&T) may coexist. - Exactly one mutable reference (
&mut T) — and while it exists, no other reference (mutable or immutable) is allowed.
This is “aliasing XOR mutability”: you can’t have multiple writers, and you can’t read through one alias while another could be writing. The borrow checker validates this statically for the entire lifetime of each reference, so data races become a compile error instead of a runtime hazard.
let mut v = String::from("hi");
let r1 = &v; // OK
let r2 = &v; // OK — multiple immutable refs fine
let r3 = &mut v; // ERROR — r1, r2 still alive
The interview answer: one &mut T OR unlimited &T at any time — never both — enforced at compile time to prevent data races.
3. What happens to a value when its owning variable goes out of scope in Rust?
Answer: Rust automatically calls the value’s drop to free its resources immediately — no garbage collector.
Rust uses RAII (Resource Acquisition Is Initialization), the same pattern C++ uses: ownership of a resource is tied to the lifetime of a variable. When the owner goes out of scope, the compiler inserts a call to Drop::drop at that point — freeing heap memory, closing files, releasing locks, etc., deterministically and immediately.
{
let s = String::from("hello"); // allocates on the heap
} // s goes out of scope → drop runs → memory freed
Key consequences:
- No runtime garbage collector — cleanup happens at a known point in the code.
- Resources are freed when the owner dies, and the compiler guarantees drop runs exactly once.
- You can implement the
Droptrait to run custom cleanup, but the memory management is automatic.
The interview answer: when an owner goes out of scope, drop is invoked automatically, freeing resources immediately (RAII, no GC).
4. What will happen when trying to compile the following code?
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
}
Answer: A compile-time error — “use of moved value: s1”.
String does not implement the Copy trait (it owns heap memory), so let s2 = s1; is a move: ownership of the string transfers from s1 to s2. After the move, s1 is invalid — the compiler tracks this and rejects println!("{}", s1) with a compile error about using a moved value.
This is Rust’s ownership system preventing a double-free: without moves, both s1 and s2 would try to free the same heap buffer at scope exit. The fix options:
- Don’t use
s1afterward. - Clone:
let s2 = s1.clone();— copies the data (deep copy). - Borrow:
let s2 = &s1;— reference instead of ownership transfer.
Note: Copy types (integers, bool, char) don’t move — let a = b; copies them, and both remain usable. String isn’t Copy, so it moves. The interview answer: compile error — s1 was moved into s2, so using s1 afterward is rejected.
5. What is the key functional difference between Option<T> and Result<T, E> in Rust?
Answer: Option<T> models presence or absence (Some/None); Result<T, E> models success or failure (Ok/Err).
Both are enums, but they express different intents:
enum Option<T> { Some(T), None } // "may or may not have a value"
enum Result<T, E> { Ok(T), Err(E) } // "may succeed with T or fail with E"
Option<T>replaces null pointers and “absent value” patterns:first_element(),find(), dictionary lookup.Nonemeans “nothing here” — not an error.Result<T, E>carries the reason for failure inErr(E):File::open→Result<File, io::Error>,parse→Result<f64, ParseFloatError>. The error type lets you distinguish and propagate failures.
The language supports both ergonomically: ? works on both (converting None/Err into early returns), and .unwrap(), .map(), .unwrap_or() etc. exist for both. The interview answer: Option = value/absence; Result = success/failure with an error payload.
6. How does the ? operator behave when used on a Result<T, E> expression inside a function?
Answer: If it’s Ok(v), ? unwraps to v; if it’s Err(e), ? returns Err early from the function (converting via From if needed).
? is syntactic sugar for the propagation pattern:
Ok(v)→ the expression evaluates tov, execution continues.Err(e)→ the function returns immediately withErr(From::from(e)). TheFromconversion lets different error types be unified into the function’s return error type:
fn read_config() -> Result<Config, MyError> {
let text = std::fs::read_to_string("config.toml")?; // io::Error → MyError via From
let cfg: Config = toml::from_str(&text)?; // parse error → MyError via From
Ok(cfg)
}
So ? keeps code linear instead of nested match arms. Two requirements: the enclosing function must return a Result (or Option, or something implementing FromResidual), and the From conversion must exist. (Outside a Result-returning function, ? is a compile error.) The interview answer: Ok(v) unwraps to v; Err(e) early-returns Err(From::from(e)) from the function.
7. Which smart pointer provides shared ownership by maintaining a reference counter on the heap?
Answer: Rc<T> (Reference Counted) — single-threaded shared ownership with a reference count.
Rc<T> lets multiple variables own the same value. It keeps a reference count on the heap; each Rc::clone(&r) bumps the count, and when the count drops to zero, the value is deallocated.
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // refcount 2 — both a and b own it
Why not always use it? Two constraints:
- Not thread-safe — the counter isn’t atomic; use
Arc<T>(atomic refcount) when sharing across threads. - Immutable —
Rc<T>gives shared immutable access (aliasing). Combine withRefCell<T>(Rc<RefCell<T>>) for interior mutability.
The four choices in the question: Box<T> = single ownership on the heap; Rc<T> = shared single-threaded ownership via refcount; Arc<T> = shared multi-threaded ownership via atomic refcount; RefCell<T> = interior mutability, not ownership. The interview answer: Rc<T> — heap reference counter for single-threaded shared ownership (use Arc<T> across threads).
8. What does Box<T> do in Rust?
Answer: It allocates the value on the heap and provides an owned, fixed-size pointer to it.
Box<T> is Rust’s simplest smart pointer: it owns a T stored on the heap while the Box itself sits on the stack (just a pointer-sized value).
let b = Box::new(5); // 5 lives on the heap
let stack = 5; // this stays on the stack
When Box goes out of scope, the heap value is dropped and the memory freed (RAII). Key uses:
- Recursive types: a type that contains itself (linked list, tree) can’t have infinite stack size —
Boxgives a finite pointer indirection. - Large data / trait objects: move a big value or a
dyn Trait(unsized type) behind a pointer. - Indirection: copy cheaply (just the pointer).
The interview answer: Box<T> allocates T on the heap and owns it through a fixed-size pointer, freeing it automatically on drop.
9. What will be the output of this code snippet?
fn main() {
let mut x = 5;
let y = &x;
let z = &x;
println!("{} and {}", y, z);
}
Answer: Compiles and prints 5 and 5.
The borrow rule allows any number of immutable references to coexist. Here y and z are both &x (immutable borrows) — two immutable references to the same value, which is perfectly legal. The mut on x only matters if you later need &mut x; it doesn’t prevent shared immutable borrows.
So the code compiles cleanly and prints 5 and 5. (If you then added let w = &mut x; while y/z were still alive, that would be a compile error — a mutable borrow while immutable borrows exist.) The interview answer: compiles fine, prints 5 and 5 — multiple immutable borrows are allowed.
10. What is a “data race” in Rust, and how does the compiler handle it?
Answer: A data race is two or more threads accessing the same memory concurrently with at least one write and no synchronization; Rust rejects it at compile time.
A data race is a specific, serious bug: multiple accesses to the same location, at least one a write, overlapping in time, with no ordering/synchronization between them — in C/C++ this is undefined behavior. Rust’s ownership and borrowing rules make data races impossible in safe code:
- A value can only be accessed through one
&mut(exclusive write) or many&(shared reads) at a time. - Across threads,
Send/Syncbounds ensure you can’t share non-thread-safe types between threads.
So the data-race pattern is a compile error before the program even runs:
// safe Rust cannot compile this
let mut x = 0;
thread::spawn(move || { x += 1; }); // compile error unless x is a Mutex/Arc/atomic
For genuinely shared mutable state, Rust forces you through synchronized primitives (Mutex, RwLock, atomics, channels). The interview answer: a race with concurrent read+write and no sync; Rust’s ownership/borrow rules plus Send/Sync prevent it at compile time.
Premium Content
Unlock Top 10 - Part 1 and all premium lessons with a subscription.
From ₹199.99/year — See plans