← Table of contents

2.3 Passing the Problem Upward

A hammer does not decide whether the wood is fit. Its business is the nail; everything else is the concern of the carpenter holding the handle. A tool that makes decisions on behalf of the one holding it works quietly - and in doing so appropriates someone else’s context. Someone else’s context has a habit of turning out to matter exactly when it was never asked.

The Attempt

In the previous chapter .unwrap() appeared on every call to Task::new in main: “the task titles are known in advance, there will be no errors.” While data comes from code, this is honest. The moment it starts arriving from outside, “known in advance” will no longer be true.

The first step is to remove .unwrap() and pass the error upward. Rust has the ? operator for this. Try replacing one call:

fn main() {
    let mut tasks: Vec<Task> = Vec::new();
    add(&mut tasks, Task::new(1, "Buy coffee")?);

The compiler notices:

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
              (or another type that implements `FromResidual`)
  --> src/main.rs:XX:XX
   |
XX | fn main() {
   | --------- this function should return `Result` or `Option` to accept `?`
...
XX |     add(&mut tasks, Task::new(1, "Buy coffee")?);
   |                                               ^ cannot use the `?` operator in a function that returns `()`
   |
   = help: the trait `FromResidual<Result<Infallible, TqError>>` is not implemented for `()`

main returns (). The ? operator requires something else.

What the Compiler Knows

? is shorthand for handling a Result. The code:

Task::new(1, "Buy coffee")?

expands to roughly:

match Task::new(1, "Buy coffee") {
    Ok(task) => task,
    Err(e) => return Err(e),
}

If Ok(task) - continue, the value is available. If Err(e) - return the error to the caller immediately. This is exactly why ? requires the current function to return a Result: it needs somewhere to send the error.

In Rust, main can return Result<(), E> as long as E implements Debug. Change the signature:

fn main() -> Result<(), TqError> {

Now ? has somewhere to forward errors. If main finishes with Err, Rust will print the value through Debug and exit with a non-zero code.

The Enchantment

Replace .unwrap() with ? on every call to Task::new, and add an explicit success at the end:

fn main() -> Result<(), TqError> {
    let mut tasks: Vec<Task> = Vec::new();

    add(&mut tasks, Task::new(1, "Buy coffee")?);
    list(&tasks);

    add(&mut tasks, Task::new(2, "Buy milk")?);
    add(&mut tasks, Task::new(3, "Buy eggs")?);

    let task = &mut tasks[0];
    task.complete();

    if task.is_done() {
        println!("Task 1 done");
    }

    list(&tasks[1..]);

    match get(&tasks, 1) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    match get(&tasks, 99) {
        Some(task) => println!("#{}: {}", task.id, task.title),
        None => println!("not found"),
    }

    Ok(())
}

Ok(()) at the end is the explicit success. Without it, a function with return type Result<(), TqError> would not compile: the body must return a Result, not ().

The logic of Task::new has not changed - the tests from the previous chapter in tq/ still pass.

make ci passes. Errors from Task::new are no longer resolved on the spot - they travel upward, to whoever knows what to do with them.

The complete tq code for this chapter is in 2-honest/03-passing-the-problem-upward/.


Lore: .unwrap(), ?, and explicit match

Three ways to work with a Result:

In main, the chain ends - the error has nowhere further to go. So ? is often used for intermediate calls, and main is simply allowed to return Err, printing through Debug.