Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Error Handling
RUST

Error Handling

Practice 9 Rust questions covering Result, Option, panic, error propagation, the ? operator, and robust error handling.

1. 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. None means “nothing here” — not an error.
  • Result<T, E> carries the reason for failure in Err(E): File::openResult<File, io::Error>, parseResult<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.

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. None means “nothing here” — not an error.
  • Result<T, E> carries the reason for failure in Err(E): File::openResult<File, io::Error>, parseResult<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.

2. 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 to v, execution continues.
  • Err(e) → the function returns immediately with Err(From::from(e)). The From conversion 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.

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 to v, execution continues.
  • Err(e) → the function returns immediately with Err(From::from(e)). The From conversion 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.

3. What will the following code produce?

fn main() {
    let mut x = 10;
    let y = &mut x;
    *y += 5;
    println!("{}", x);
}

Answer: Prints 15 — the mutable borrow ends before println!.

Under NLL (Non-Lexical Lifetimes), a borrow lives only until its last use, not until the end of the block. Here:

  1. y borrows x mutably; *y += 5 adds 5 → x becomes 15.
  2. After that line, y is never used again — the mutable borrow ends.
  3. println!("{}", x) then immutably borrows x — legal, since no mutable borrow is active.

Output: 15. (With the old lexical-lifetime rules, this would have been a compile error; NLL — stable since Rust 2018 — makes it work.) The interview answer: 15 — NLL ends the mutable borrow after its last use, so println! can read x.

Answer:

Prints 15 — the mutable borrow ends before println!.

Under NLL (Non-Lexical Lifetimes), a borrow lives only until its last use, not until the end of the block. Here:

  1. y borrows x mutably; *y += 5 adds 5 → x becomes 15.
  2. After that line, y is never used again — the mutable borrow ends.
  3. println!("{}", x) then immutably borrows x — legal, since no mutable borrow is active.

Output: 15. (With the old lexical-lifetime rules, this would have been a compile error; NLL — stable since Rust 2018 — makes it work.) The interview answer: 15 — NLL ends the mutable borrow after its last use, so println! can read x.

4. What is the difference between panic! and returning Result::Err?

Answer: panic! is for unrecoverable errors (unwinds/aborts the program); Result::Err is for recoverable errors the caller should handle.

  • panic! — signals a bug or impossible state the program can’t recover from (e.g., index out of bounds, violated invariant). It unwinds the current thread’s stack (running destructors) or aborts, depending on config. Callers don’t handle it — the program fails.
  • Result<T, E> — the expected failure mode: file missing, parse failed, I/O error. The function returns Ok/Err, and the caller decides what to do (propagate with ?, match, unwrap).

Rust’s guidance is explicit: use Result for errors you anticipate and want handled; reserve panic! for programmer errors and unrecoverable states. Libraries overwhelmingly return Result rather than panicking, so callers keep control. The interview answer: panic! = unrecoverable program failure (unwind/abort); Result::Err = recoverable error the caller must handle.

Answer:

panic! is for unrecoverable errors (unwinds/aborts the program); Result::Err is for recoverable errors the caller should handle.

  • panic! — signals a bug or impossible state the program can’t recover from (e.g., index out of bounds, violated invariant). It unwinds the current thread’s stack (running destructors) or aborts, depending on config. Callers don’t handle it — the program fails.
  • Result<T, E> — the expected failure mode: file missing, parse failed, I/O error. The function returns Ok/Err, and the caller decides what to do (propagate with ?, match, unwrap).

Rust’s guidance is explicit: use Result for errors you anticipate and want handled; reserve panic! for programmer errors and unrecoverable states. Libraries overwhelmingly return Result rather than panicking, so callers keep control. The interview answer: panic! = unrecoverable program failure (unwind/abort); Result::Err = recoverable error the caller must handle.

5. What is the default mechanism for handling a panic! in Rust binaries?

Answer: Stack unwinding — Rust walks up the stack, running destructors, until it aborts the program (or the panic is caught).

By default, a panic causes the runtime to unwind the stack of the panicking thread:

  • Each frame’s local variables are dropped (their Drop implementations run), releasing resources.
  • Unwinding continues until the thread’s top, where the program aborts (or catch_unwind intercepts it).

This is configurable in Cargo.toml — setting panic = "abort" skips unwinding entirely and aborts immediately (smaller binaries, faster, but no destructors run):

[profile.release]
panic = "abort"

Default is unwind, giving a chance to clean up. The interview answer: by default Rust unwinds the stack, calling destructors, before aborting; panic = "abort" can change that to immediate abort.

Answer:

Stack unwinding — Rust walks up the stack, running destructors, until it aborts the program (or the panic is caught).

By default, a panic causes the runtime to unwind the stack of the panicking thread:

  • Each frame’s local variables are dropped (their Drop implementations run), releasing resources.
  • Unwinding continues until the thread’s top, where the program aborts (or catch_unwind intercepts it).

This is configurable in Cargo.toml — setting panic = "abort" skips unwinding entirely and aborts immediately (smaller binaries, faster, but no destructors run):

[profile.release]
panic = "abort"

Default is unwind, giving a chance to clean up. The interview answer: by default Rust unwinds the stack, calling destructors, before aborting; panic = "abort" can change that to immediate abort.

6. What happens when an arithmetic operation overflows in debug mode versus release mode?

Answer: Debug panics at runtime; release wraps (two’s complement) silently.

  • Debug builds: overflow checks are on. let x = 255u8 + 1; panics with “attempt to add with overflow” — catching the bug early.
  • Release builds (--release): checks are disabled for performance; the operation wraps around using two’s complement (255 + 1 wraps to 0).
let a: u8 = 255 + 1;     // debug: panic   |   release: 0

If you want deterministic behavior regardless of build, use explicit methods: wrapping_add, checked_add (returns Option), saturating_add, or overflowing_add. The interview answer: debug panics on overflow; release wraps silently — use checked_/wrapping_/saturating_ methods for explicit control.

Answer:

Debug panics at runtime; release wraps (two’s complement) silently.

  • Debug builds: overflow checks are on. let x = 255u8 + 1; panics with “attempt to add with overflow” — catching the bug early.
  • Release builds (--release): checks are disabled for performance; the operation wraps around using two’s complement (255 + 1 wraps to 0).
let a: u8 = 255 + 1;     // debug: panic   |   release: 0

If you want deterministic behavior regardless of build, use explicit methods: wrapping_add, checked_add (returns Option), saturating_add, or overflowing_add. The interview answer: debug panics on overflow; release wraps silently — use checked_/wrapping_/saturating_ methods for explicit control.

7. What is the output of println!(”{}”, 10 / 4); in Rust?

Output: 2.

Both operands are integers, so this is integer division: the fractional part is truncated (toward zero for signed). 10 / 4 = 2.5 → truncated to 2. Output: 2.

To get 2.5, at least one operand must be floating point: 10.0 / 4 or 10 / 4.0. The interview answer: 2 — integer division truncates the remainder.

Answer:

2.

Both operands are integers, so this is integer division: the fractional part is truncated (toward zero for signed). 10 / 4 = 2.5 → truncated to 2. Output: 2.

To get 2.5, at least one operand must be floating point: 10.0 / 4 or 10 / 4.0. The interview answer: 2 — integer division truncates the remainder.

8. How does Rust handle type conversion between primitive types (e.g., i32 to i64)?

Answer: Explicitly — via the as keyword or the From/Into traits; no implicit coercions.

Rust deliberately avoids implicit primitive conversions (they hide precision bugs, like i64 silently truncating to i32):

  • as casts: x as i64, y as f64, c as u8 — explicit, can truncate (must be intentional).
  • From/Into: i64::from(x) / x.into() — safe, lossless conversions (e.g., i32i64 is lossless so From exists; the reverse i64i32 is not From).
let i: i32 = 5;
let big: i64 = i as i64;      // explicit cast
let big2: i64 = i.into();     // via Into (lossless)

When a lossless conversion exists, prefer From/Into; use as when truncation/reinterpretation is intended. The interview answer: explicit — as for casts, From/Into for safe conversions; no implicit primitive coercions.

Answer:

Explicitly — via the as keyword or the From/Into traits; no implicit coercions.

Rust deliberately avoids implicit primitive conversions (they hide precision bugs, like i64 silently truncating to i32):

  • as casts: x as i64, y as f64, c as u8 — explicit, can truncate (must be intentional).
  • From/Into: i64::from(x) / x.into() — safe, lossless conversions (e.g., i32i64 is lossless so From exists; the reverse i64i32 is not From).
let i: i32 = 5;
let big: i64 = i as i64;      // explicit cast
let big2: i64 = i.into();     // via Into (lossless)

When a lossless conversion exists, prefer From/Into; use as when truncation/reinterpretation is intended. The interview answer: explicit — as for casts, From/Into for safe conversions; no implicit primitive coercions.

9. What is the role of std::panic::catch_unwind?

Answer: It catches a panicking closure’s stack unwinding, returning Result, so a panic doesn’t propagate past a boundary (e.g., FFI).

let result = std::panic::catch_unwind(|| {
    // code that might panic
});
// Ok(value) or Err(Box<dyn Any + Send>)

Uses and constraints:

  • FFI safety: a panic unwinding across a C boundary is undefined behavior; wrap the boundary call in catch_unwind to contain it.
  • Isolation: catch panics in plugin/task code and continue.
  • Limitations: only catches unwinding panics (not panic = "abort" builds), and only across the current thread (panics in other threads aren’t caught).
  • The closure must be UnwindSafe (a &mut that’s already borrowed may not be).

The interview answer: catch_unwind runs a closure and captures an unwinding panic as Result, preventing unwinding past FFI/thread boundaries.

Answer:

It catches a panicking closure’s stack unwinding, returning Result, so a panic doesn’t propagate past a boundary (e.g., FFI).

let result = std::panic::catch_unwind(|| {
    // code that might panic
});
// Ok(value) or Err(Box<dyn Any + Send>)

Uses and constraints:

  • FFI safety: a panic unwinding across a C boundary is undefined behavior; wrap the boundary call in catch_unwind to contain it.
  • Isolation: catch panics in plugin/task code and continue.
  • Limitations: only catches unwinding panics (not panic = "abort" builds), and only across the current thread (panics in other threads aren’t caught).
  • The closure must be UnwindSafe (a &mut that’s already borrowed may not be).

The interview answer: catch_unwind runs a closure and captures an unwinding panic as Result, preventing unwinding past FFI/thread boundaries.

My Private Notes

Notes are auto-saved locally to this device.