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
tqcode for this chapter is in2-honest/03-passing-the-problem-upward/.
Lore: .unwrap(), ?, and explicit match
Three ways to work with a Result:
-
.unwrap()- “I am certain there is anOkhere. If not - let the program crash.” Suitable in tests and in code with known-good data. -
?- “If there is an error, I pass it upward.” Only works in functions that returnResult. Removes noise, preserves meaning. -
match- “I handle both cases here.” Needed when the error requires specific behaviour: log it, substitute a default, try again.
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.