← Table of contents

2.2 When Things Go Wrong

Saying “not found” is one skill. Saying “I won’t take this, and here is why” is another. To a craftsman the difference is obvious; to most tools it is not at all. A tool that can only drop a workpiece silently when it turns out to be defective is not much more honest than one that simply panics. The difference is only in who hears the crash.

What You Need

Task::new(1, "") creates a task with no title and raises no objection. Later it will appear in the list, occupy its place, and there will be no way to know where it came from or what it means. A way is needed to say at the entrance: this task is not acceptable - and to explain why.

A type is needed that carries either a value or a reason for refusal - and leaves the decision to the calling code.

The Build

Start with the error. TqError is an enumeration of refusal reasons. Start with one:

#[derive(Debug)]
enum TqError {
    EmptyTitle,
}

Result<T, E> is Rust’s standard type for operations that can succeed or fail:

Change Task::new. Before: -> Task. After: -> Result<Task, TqError>:

fn new(id: u64, title: &str) -> Result<Task, TqError> {
    if title.is_empty() {
        return Err(TqError::EmptyTitle);
    }
    Ok(Task {
        id,
        title: String::from(title),
        status: Status::Todo,
    })
}

If the title is empty, return Err(TqError::EmptyTitle). Otherwise, wrap the task in Ok(...). Both variants have the same type: Result<Task, TqError>.

Now anyone who calls Task::new receives not a task but a Result - and must handle both cases explicitly. Silently ignoring the error is no longer possible.

Where a task is known to be valid, .unwrap() can be used for now:

add(&mut tasks, Task::new(1, "Buy coffee").unwrap());

Where an error is possible, handle it explicitly:

match Task::new(4, "") {
    Ok(_) => println!("task created"),
    Err(e) => println!("error: {:?}", e),
}

Two variants - two tests:

#[test]
fn new_task_rejects_empty_title() {
    assert!(Task::new(1, "").is_err());
}

#[test]
fn new_task_accepts_valid_title() {
    assert!(Task::new(1, "Buy coffee").is_ok());
}

The full test suite is in tq/.

The Result

main below shows only what is new - .unwrap() on valid tasks and explicit error handling. The full accumulated code is in tq/.

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

    add(&mut tasks, Task::new(1, "Buy coffee").unwrap());
    add(&mut tasks, Task::new(2, "Buy milk").unwrap());
    add(&mut tasks, Task::new(3, "Buy eggs").unwrap());

    match Task::new(4, "") {
        Ok(_) => println!("task created"),
        Err(e) => println!("error: {:?}", e),
    }
}
error: EmptyTitle

make ci passes. Task::new now refuses to accept an empty title - and explains why.

The complete tq code for this chapter is in 2-honest/02-when-things-go-wrong/.


Lore: .unwrap() and When to Use It

.unwrap() is an explicit choice: “I am certain there is an Ok here, and if not - let the program crash.” In the code above this is justified: the task titles are known in advance and are definitely not empty. In code that accepts data from outside, .unwrap() is a risk. Chapter 2.3 will introduce the ? operator, which removes the need for this choice in most cases.