Rust error handling interview questions, with answers
Rust splits failures in two. Problems a program should expect, such as bad input or a missing file, are values of type Result that the caller must handle; bugs and impossible states panic. Interviewers ask how that split works in practice: how errors travel up the call stack with the ? operator, how to define an error type, and how to get a value out of an Option or a Result without crashing.
The answers below cover Result, the difference from Option, the ? operator, custom errors, panic!, fallbacks and conversions between the two types, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.How does Rust handle errors?
In short: Recoverable errors are values of type Result<T, E>, returned and handled explicitly; unrecoverable ones, which indicate bugs, use panic!.
Result<T, E> is an enum with two variants: Ok(T) for success and Err(E) for failure. A function that can fail returns one, and because the success value is inside the Result, the caller cannot use it without deciding what happens in the error case: matching on it, propagating it with ?, or supplying a fallback. The compiler also warns when a Result is ignored entirely. There are no exceptions and no hidden control flow; the error path is visible in every signature. panic! exists for situations the program cannot sensibly recover from, and by default it unwinds the current thread.
let r = "7x".parse::<i32>(); match r { Ok(n) => println!("{n}"), Err(_) => println!("bad"), } // bad
2.What is the difference between Option and Result in Rust?
In short: Option says a value may be absent, with no reason given; Result says an operation may fail and carries an error value explaining why.
Option<T> is Some(T) or None, suited to lookups where absence is normal and needs no explanation, such as finding an element or reading an optional field. Result<T, E> is Ok(T) or Err(E), suited to operations where the caller may want to know what went wrong, such as parsing, I/O or validation, and the E can be a detailed error type. Both have the same toolkit: map, and_then, unwrap_or, the ? operator and conversions between them. Choosing the right one documents the function: a None return says nothing failed, while an Err says something did.
let a: Option<u8> = None; let b: Result<u8, &str> = Err("empty"); println!("{:?} {:?}", a, b); // None Err("empty")
3.What does the ? operator do in Rust?
In short: Applied to a Result or Option, ? unwraps the success value, or returns the error or None from the current function immediately.
value? replaces a common match: if the value is Ok(x), the expression evaluates to x and execution continues; if it is Err(e), the enclosing function returns that error at once. On an Option, ? returns None early in the same way. The function's own return type must be compatible, which is why ? is used inside functions returning Result or Option, and why main can use it when declared to return a Result. Box<dyn Error>, used below through the Res alias, is a convenient return type that can hold any error, so one function can apply ? to operations with different error types. In area, the second parse fails for "x", and ? returns that error before the multiplication runs.
use std::error::Error; type Res<T> = Result<T, Box<dyn Error>>; fn area(w: &str, h: &str) -> Res<u32> { let w: u32 = w.parse()?; let h: u32 = h.parse()?; Ok(w * h) } fn main() { let a = area("3", "4"); println!("{a:?}"); // Ok(12) let e = area("3", "x"); println!("{}", e.is_err()); // true }
4.How do you define a custom error type in Rust?
In short: Define a struct or enum, implement Display for its message and the std::error::Error trait, and return it in Result's error position.
A custom error type lets callers tell failures apart and read their details. It is an ordinary struct or enum, usually deriving Debug, with a Display implementation that writes the message users see, and an empty impl of std::error::Error, whose methods all have defaults. An enum with one variant per failure is common for a library, so callers can match on the kind of error. Implementing Error means the type also works with Box<dyn Error> and with the source chain that reports underlying causes. Crates such as thiserror generate this boilerplate from attributes.
use std::error::Error; use std::fmt; #[derive(Debug)] struct AgeErr(i32); impl fmt::Display for AgeErr { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { let a = self.0; write!(f, "age {a}") } } impl Error for AgeErr {} fn check(a: i32) -> Result<i32, AgeErr> { if a < 0 { return Err(AgeErr(a)); } Ok(a) } fn main() { if let Err(e) = check(-3) { println!("{e}"); // age -3 } }
5.What does panic! do in Rust?
In short: panic! stops the current thread with a message, unwinding its stack and running destructors; if it is the main thread, the program exits with a failure code.
A panic signals a bug or a state the program cannot continue from, such as an index out of bounds or a broken invariant. By default the panicking thread unwinds: it walks back up the stack, dropping every value so that locks and files are released, and then ends. A panic in the main thread ends the program with exit code 101 after printing the message and location; a panic in another thread ends only that thread, and join on it returns an Err. A build can be configured with panic = "abort" to skip unwinding and stop immediately, which makes binaries smaller. panic! is not for failures a caller should handle; those return Result.
let v: Vec<u8> = Vec::new(); if v.is_empty() { panic!("no data"); } // panics
6.How do you get a value out of an Option or Result without panicking?
In short: Use a method that supplies a fallback, such as unwrap_or, unwrap_or_else or unwrap_or_default, or handle both cases with match or if let.
unwrap_or(80) returns the value inside Some or Ok, and the given default otherwise, evaluating the default eagerly. unwrap_or_else(|| ...) takes a closure and calls it only when needed, which matters when the fallback is expensive or has side effects. unwrap_or_default uses the type's Default value, 0 for numbers or an empty String, as the parse below does. map and and_then transform the success value without unwrapping, and match or if let handle both branches explicitly. These keep the absence or the error visible in the code while still producing a usable value.
let port: Option<u16> = None; let p = port.unwrap_or(80); let n = "x".parse::<i32>() .unwrap_or_default(); println!("{p} {n}"); // 80 0
7.How do you convert between Option and Result in Rust?
In short: Option::ok_or turns None into a chosen error, and Result::ok turns a Result into an Option, discarding the error.
Code often mixes the two, such as a map lookup that returns an Option inside a function that returns a Result. opt.ok_or(err) converts Some(v) into Ok(v) and None into Err(err), so the missing value can be reported with ?; ok_or_else builds the error lazily. In the other direction, res.ok() keeps the success value as Some and turns any Err into None, which is useful when the reason for a failure does not matter. transpose swaps an Option<Result<T, E>> into a Result<Option<T>, E> and back, which comes up when parsing optional fields.
let o: Option<u8> = Some(3); let r: Result<u8, &str> = o.ok_or("missing"); let back = r.ok(); println!("{r:?} {back:?}"); // Ok(3) Some(3)
How the diagnostic asks it
One question from the Rust bank, exactly as a sitting would show it. The bank has 4 on error handling and 30 across Rust.
What does this Rust program print?
use std::num::ParseIntError; fn parse_sum(a: &str, b: &str) -> Result<i32, ParseIntError> { let x: i32 = a.parse()?; let y: i32 = b.parse()?; Ok(x + y) } fn main() { println!("{:?}", parse_sum("4", "5")); println!("{}", parse_sum("4", "five").is_err()); }
- 19, then false
- 2Ok(9), then truecorrect
- 3Ok(9), then a panic: invalid digit
- 4It does not compile: ? can only be used in main
? is shorthand for a match: on Ok(value) it unwraps the value and carries on; on Err(e) it returns Err(e) from the enclosing function at once. parse_sum("4", "5") parses both and returns Ok(9), which {:?} prints as Ok(9). In parse_sum("4", "five"), the second parse fails, so ? returns that ParseIntError, and is_err() is true. 9, then false ignores that the function returns a Result. ? never panics; that is unwrap's behaviour. ? works in any function whose return type can hold the error, such as this Result, not only in main.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.