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:
Ok(value)- the operation succeeded; the result is insideErr(reason)- the operation failed; the reason is inside
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
tqcode for this chapter is in2-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.