ProgrammingSystem Programmer

Explain how error management is done through Result<T, E> and how to correctly use the question mark operator (?) without violating safety and code readability.

Pass interviews with Hintsage AI assistant

Answer.

Rust is built on explicit error management: exceptions are absent, instead, a returned value Result<T, E> is used. This ensures safety and predictability of code.

Background:

Many languages have taken the approach of exceptions, leading to unexpected situations and the need to explicitly handle exceptions at runtime. From the very beginning, Rust focused on type control, where all errors must be part of the function signature.

Problem:

The main task is to write code where errors are not ignored or masked, there are no "panic" functions, but at the same time, the code remains compact and readable. It's necessary to correctly propagate errors upward without losing information about their type, and to avoid complicating the logic.

Solution:

The key tool is the type Result<T, E> and the ? operator, which automatically "unwraps" the result: on error, it causes an immediate exit from the function with the return of the error, and on success, it returns the value.

Example code:

fn read_number(file: &str) -> Result<i32, std::io::Error> { let content = std::fs::read_to_string(file)?; let num: i32 = content.trim().parse()?; Ok(num) }

Key features:

  • Explicit declaration of all potential errors in the signature
  • The ? operator simplifies nesting (removes if-let/unwrap/expect)
  • Errors can be easily "wrapped" or transformed (via .map_err() and other methods)

Tricky questions.

Can ? be used in functions that return Unit (void)?

No, the ? operator is only allowed within functions that return Result or Option. If a function returns (), ? cannot be used.

What happens if the error types in multiple calls with ? are different?

It results in a compilation error: the error type must be clearly defined. You need to either unify all errors to the same type using .map_err(), or use thiserror, or describe an enum wrapper at the API level. Example:

fn foo() -> Result<_, MyError> { let a = bar()?; let b = baz().map_err(MyError::Baz)?; Ok() }

What is dangerous about .unwrap() in the inner logic?

There is a common misconception that in "main" code, unwrap() can be safely used, "it definitely won't panic". In reality, even a small invisible error can lead to a runtime panic — compromising the safety of the program.

Common mistakes and anti-patterns

  • Catching all errors through unwrap/expect, especially in inner logic
  • Loss of context when using map_err(|_| …), when the error is wrapped without preserving the original information
  • Excessive nesting when manually handling each error without ?

Real-life examples

Negative case

Production code retained many unwrap() after quick debugging. As a result, the application crashed on any parsing failure due to incorrect user input.

Pros:

  • Fast debugging

Cons:

  • Potential application crash, hard to find the cause

Positive case

Used the full stack of Result<T, E> with explicit error descriptions and applied only ? and .map_err(), preserving all failure information.

Pros:

  • Ease of debugging, predictable behavior

Cons:

  • Slightly more "noisy" error types