December Code

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. 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. 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. 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. 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. 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. 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. 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.

Error Handling · mediumRUST-016

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());
}
  1. 19, then false
  2. 2Ok(9), then truecorrect
  3. 3Ok(9), then a panic: invalid digit
  4. 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.

What the readiness test measures · how the score is computed

By Harshit · updated