Skip to content
Languages

10 Rust Errors and Ecosystem Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Errors and Ecosystem material into 10 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

10 min read10 detailed answersReviewed Aug 24, 2026
What to remember

Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.

Question set

10 detailed answers

01

When do you use Option<T> vs Result<T, E>?

Short answer: Option<T> — when a value may simply be absent and the reason doesn't matter (Some/None). Result<T, E> — when an operation can fail and the caller needs the cause (Ok/Err).

In depth:

  1. Option — models "the value may not be there": HashMap::get missed the key, Vec::first on an empty vector. None carries no reason.
  2. Result — models "the operation failed": File::open didn't find the file, str::parse couldn't parse. Err(e) explains what went wrong.
  3. Conversion — they cross over easily: option.ok_or(err) turns None into Err, and result.ok() drops the error and yields an Option.
Type Meaning Typical API
Option value may be absent HashMap::get, Vec::first
Result<T, E> operation may fail File::open, str::parse

⚠️ Common mistake: returning Result where absence is normal (or Option where the caller needs the cause). "Key not found" is an Option, not an error.

02

How does the ? operator work, and what does the From trait have to do with it?

Short answer: ? is an early return: on Ok(v) it unwraps the value, on Err(e) it does return Err(From::from(e)). That implicit From conversion is exactly what lets ? "just work" across layers with different error types.

In depth:

  1. Desugaringlet v = expr?; expands into a match whose Err arm returns from the function immediately.
  2. From::from — it converts the error from the inner call into the current function's error type. If an impl From<SubError> exists, the conversion happens automatically.
  3. Why it's handy — a custom error enum with a set of From impls makes ? seamless across every layer. Those impls are exactly what #[derive(thiserror::Error)] generates via #[from].
// expr? expands roughly into this:
match expr {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

⚠️ Common mistake: thinking ? is an exception and stack unwinding. It's a plain return: no throw/catch, just an early exit carrying an Err.

03

When is panic! appropriate vs returning Result? What happens on panic — unwind or abort?

Short answer: panic! is for broken invariants and bugs you can't meaningfully handle; Result is for expected errors the caller handles. By default a panic unwinds the stack, running Drop on every value; panic = "abort" in Cargo.toml instead kills the process immediately.

In depth:

  1. panic! — "this should never happen": out-of-bounds index, unwrap() on None, a violated invariant. No recovery is intended.
  2. Result — "this can go wrong, and that's fine": file not found, input didn't parse. The caller decides what to do.
  3. unwind vs abort — the default unwind unrolls the stack and runs destructors (catchable via catch_unwind). abort gives a smaller binary and is needed for no_std/embedded, but cleans up nothing.
panic! Result
When bug, broken invariant expected failure
Recovery none yes, by the caller
Example arr[99], unwrap File::open, parse

⚠️ Common mistake: unwinding a panic across an FFI boundary (extern "C"). It used to be UB; since Rust 1.81 the process simply aborts. To propagate a panic you need extern "C-unwind".

04

How does String differ from &str, and which should a function accept?

Short answer: String is an owned, growable UTF-8 buffer on the heap (ptr + len + cap, 24 bytes on a 64-bit platform). &str is a borrowed immutable view (a fat pointer ptr + len, 16 bytes) into a String's buffer, a string literal, or any valid UTF-8 range. In signatures prefer &str — it accepts both via deref coercion.

In depth:

  1. String — owns its data, can grow (push_str), lives on the heap. A ptr/len/cap triple.
  2. &str — only reads someone else's bytes, owns nothing. A fat pointer ptr/len, no cap.
  3. Arguments — take &str: &String coerces to &str automatically, so the function works with both literals and owned strings. Return String when you need to hand over ownership.
String "hi"           &str to literal "hi"
┌──────┬─────┬─────┐   ┌──────┬─────┐
│ ptr  │len=2│cap=8│   │ ptr  │len=2│   (16 bytes, no cap)
└──┬───┴─────┴─────┘   └──┬───┴─────┘
   ▼ heap                 ▼ .rodata (in the binary)
  [h][i][ ][ ]...        [h][i]

⚠️ Common mistake: thinking &str always borrows a String. String literals ("...") live in the binary's static memory ('static), not on the heap.

05

How is Vec<T> laid out internally, and what happens on push when len == capacity?

Short answer: Vec<T> is a (ptr, len, cap) triple: a pointer to a heap buffer, the number of used elements, and the capacity. When push hits len == cap, the vector allocates a new buffer (usually twice as large), moves every element over, and frees the old one — old pointers and slices are invalidated.

In depth:

  1. Layout — 24 bytes on the stack (on a 64-bit platform): ptr to the heap, len, cap. The elements themselves live on the heap.
  2. Growth — on overflow of cap, a reallocation with amortized doubling: O(1) average per push, even though an individual push may copy the whole buffer.
  3. Invalidation — after a reallocation the old address is dead. That's exactly why the borrow checker forbids holding &vec[0] across a push: the reference could point into freed memory.
push when len == cap = 4:

before: ptr ─► [a][b][c][d]                (cap=4, len=4)
               ╲ realloc + move
after:  ptr ─► [a][b][c][d][e][_][_][_]    (cap=8, len=5)
               old buffer [a..d] freed ✗

⚠️ Common mistake: holding a reference/slice into the vector and calling push — the compiler rejects it not out of spite but because the reallocation would leave the reference dangling. Reserve up front with Vec::with_capacity.

06

How do macro_rules! and procedural macros differ, and when do you pick each?

Short answer: macro_rules! are declarative macros: pattern matching over token trees, hygienic by default, great for repetitive patterns and small DSLs. Procedural macros are functions that run arbitrary Rust code over a TokenStream at compile time; they're required for derive, attribute, and function-like macros with real parsing.

In depth:

  1. macro_rules! — you write a set of (pattern) => (expansion) rules. Hygienic by default: identifiers the macro introduces don't clash with the call site's code.
  2. Procedural — a separate crate (proc-macro = true); they receive a TokenStream, parse it (usually via syn) and generate a new one (quote). Three kinds: #[derive(...)], attribute #[route(...)], function-like sql!(...).
  3. Choosing — plain code duplication → macro_rules!; you need to parse a type's structure and generate code (like serde) → proc-macro.
macro_rules! proc-macro
Model token matching arbitrary code over TokenStream
Hygiene by default manual (Span)
Lives inline separate crate
For DSLs, templates derive, attributes, parsing

⚠️ Common mistake: reaching for a proc-macro where macro_rules! would do. Mentioning hygiene in an interview is a strong-candidate signal.

07

Rust has no reflection — so how does serde serialize structs?

Short answer: #[derive(Serialize)] is a procedural macro: at compile time it generates a per-type impl Serialize that calls the methods of a format-specific Serializer (from serde_json, bincode, etc.). It's all static — no runtime introspection.

In depth:

  1. Codegenderive inspects the struct's fields at compile time and writes the serialize body for you: one call per field.
  2. data/format splitserde defines the data model and the Serialize/Serializer traits; a concrete format (JSON, YAML, MessagePack) is a separate Serializer implementation. One derive works with every format.
  3. Zero runtime cost — no type tags, no field dictionary, no reflection. The generated code is straight-line and inlines.
#[derive(Serialize)]        compile            serde_json::to_string
struct User{ id, name }  ─────────────►  impl Serialize for User ──► Serializer
        │                 (proc-macro)      serialize_field("id")     builds JSON
        └── static code, no reflection at runtime

⚠️ Common mistake: assuming serde walks fields at runtime like reflection in Java/Python. The whole traversal is baked into code at compile time.

08

What are Cargo workspaces and feature flags for? What is feature unification and why is it dangerous?

Short answer: A workspace is several crates sharing one Cargo.lock and target/, which speeds up builds and keeps versions consistent. Feature flags are additive, compile-time options. Feature unification: Cargo unions the enabled features of each dependency across the whole build graph — if one crate turns a feature on, it's on for every consumer of that dependency.

In depth:

  1. Workspace — a shared lock file and build directory; crates build together and dependency versions are unified.
  2. Features — conditional compilation (#[cfg(feature = "...")]), optional dependencies. They must be additive: enabling a feature only adds, never breaks the API.
  3. Unification — Cargo compiles each dependency once with the union of all requested features. The danger: a third-party crate can enable a feature (say std) that breaks your no_std build, or silently pulls in extra code.
       ┌─ crate A ─ needs serde/derive ─┐
graph ─┤                                ├─► serde builds ONCE
       └─ crate B ─ needs serde/std  ───┘   with features { derive, std }
                                            (union, not intersection)

⚠️ Common mistake: assuming features are isolated per crate. Resolver v2 separates build/dev dependencies, but in the normal graph features still unify — which is why they must be kept strictly additive.

09

What does #[non_exhaustive] do, and why would a library put it on a public enum?

Short answer: #[non_exhaustive] warns consumer crates that the type may still grow. On an enum it forces a wildcard _ arm in every match; on a struct (or an individual variant) it forbids constructing the value with a literal. That lets the library author add new variants and fields later without breaking semver compatibility.

In depth:

  1. What it forbids — on an enum: outside the crate you can't match exhaustively without _ (the variants themselves can still be constructed — ErrorKind::NotFound is valid). On a struct or an enum variant: outside the crate you can't create a value with a Struct { .. } literal, and patterns must include ...
  2. Why — adding a variant to an ordinary public enum is a breaking change: users' match stops compiling. With #[non_exhaustive] the _ is already there, so a new variant is safe.
  3. Where it's used — error and config enums in libraries (std::io::ErrorKind is the classic example).
// in a library consumer the _ arm is mandatory:
match err.kind() {
    ErrorKind::NotFound => ...,
    ErrorKind::PermissionDenied => ...,
    _ => ..., // without it the code won't compile
}

⚠️ Common mistake: expecting the attribute to apply inside the defining crate too. Every restriction targets external consumers only: your own code still matches exhaustively and constructs literals, so on internal types the attribute is dead weight.

10

Why is Rust's enum called an algebraic data type? How do Option and Result fall out of that?

Short answer: an enum is a sum type: a value is exactly one of N variants, and each variant can carry its own data. A struct, by contrast, is a product type. Exhaustive match makes illegal states unrepresentable. Option and Result are ordinary two-variant enums in the standard library.

In depth:

  1. Sum vs productstruct { a, b } holds a AND b (a product of value sets); an enum holds one of its variants (a sum). Hence the term "algebraic".
  2. Illegal states unrepresentable — the compiler forces you to handle every variant in match; a forgotten case is a compile error, not a runtime bug.
  3. Option and Result — not language magic, but two two-variant enums: this is how Rust buries null (ordinary types have no "empty" state) and exceptions (an error is a value) with a single mechanism.
enum Option<T> { Some(T), None }        // either a value or nothing
enum Result<T, E> { Ok(T), Err(E) }     // either success or an error

⚠️ Common mistake: treating Option/Result as built-in special constructs. They're ordinary enums — you could declare the same yourself; only ? and syntactic sugar make them "special".

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Languages103 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers
RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS