Before you begin
Why this matters
Rust puts a possible failure into the return type, so the compiler knows about it. The bugs move to the places where code throws the failure away or turns it into a panic.
What this helps you reason about
- Follow an Err back to the caller, through match and ?.
- Spot let _ = and unwrap where a failure should have been handled.
- Turn an Err into a panic, an Option, or a message, and know what each loses.
A first-aid kit holds 3 plasters, and two patients need 2 each. The second take must fail. In Rust, a failure that a caller should expect is a value of type Result; a bug stops the program with a panic.
Already know this? The short version
- Rust passes expected failures as values: a function returns Result<T, E>, either Ok(value) or Err(error), and the caller must match it or pass it on with ?.
- ? returns the Err from the current function at once, so that function must itself return a Result (E0277 otherwise); the lines after it do not run.
- A panic jumps up the call stack and stops the thread; it is for bugs, and unwrap and expect turn an Err into one.
- let _ = throws a Result away without the unused-Result warning; ok() turns a Result into an Option and drops the error.
Words in this lesson (7)
- Result
- A type whose value is either Ok(x) for success or Err(e) for failure. Functions that can fail, such as parse, return a Result.
- call stack
- The frames of all calls that have not finished yet, with the newest on top. A new call adds a frame on top, and a return removes it. Only the newest call runs; each call below it waits for the call it made.
- Option
- A type whose value is either Some(x), holding a value, or None, holding nothing. Option<u32>, for example, is a number that may be missing.
- propagate
- To pass a failure on to your own caller instead of handling it. An exception propagates by itself; a failure returned as a value propagates only when each caller returns it again.
- ? operator
- An operator written after a call that returns a Result. It gives the value inside Ok, or returns the Err from the current function at once.
- unwrap
- A method that takes the value out of an Option or a Result. It checks while the program runs and panics when there is no value: a None, or an Err.
- panic
- Rust’s way of stopping because of a bug: the current thread stops with a message such as thread 'main' panicked at …. In a program with one thread, the program stops.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Two ways
How can a failure reach the caller?
A function that cannot give its result must tell its caller. Languages use two ways.
In the first way, the failure is a value. The function returns in the normal way, and what it returns says that it failed. The caller must look at that value to notice the failure.
In the second way, the failure is an exception. The function does not return in the normal way. The failure jumps up the call stack: the frame of the failing function ends, then its caller’s frame ends, and so on, until the failure reaches code that catches it. The lines in between are skipped.
Rust uses values. A function that can fail returns Result<T, E>: Ok(value) when it worked, or Err(error) when it failed. T is the type of the value and E the type of the error; below, they are u32 and String. format! builds a String the way println! prints. A match handles both alternatives before the number can be used.
fn take(left: u32, wanted: u32) -> Result<u32, String> {
if wanted > left {
return Err(format!("need {}, have {}", wanted, left));
}
Ok(left - wanted)
}
match take(3, 4) {
Ok(rest) => println!("left: {}", rest),
// prints failed: need 4, have 3
Err(problem) => println!("failed: {}", problem),
} A failure reaches the caller either as a value it must look at, or as an exception that jumps up the call stack. Rust uses values.
Coming from Java: There is no try and no throws. The Err comes back as an ordinary return value, and match must handle it before you can use the number.
Coming from Python: Rust has no exceptions for expected failures; the closest thing, a panic, is for bugs and is not meant to be caught.
Experiment
Follow an Err back through ?
? looks at the Result from take. For Ok, it gives the number inside. For Err, it returns that Err from treat at once. main then matches the Err.
Why it matters ? is an early return written in one character. The lines after it run only when the call worked.
Predict which lines run after the second take fails, and what is printed. Then change treat(3) to treat(4) and predict again.
fn take(left: u32, wanted: u32) -> Result<u32, String> {
if wanted > left {
return Err(format!("need {}, have {}", wanted, left));
}
Ok(left - wanted)
}
fn treat(left: u32) -> Result<u32, String> {
let after_first = take(left, 2)?;
let after_second = take(after_first, 2)?;
println!("both patients treated");
Ok(after_second)
}
fn main() {
match treat(3) {
Ok(rest) => println!("left: {}", rest),
Err(problem) => println!("failed: {}", problem),
}
}
What to look for
Step into treat and into each take. In the second take, left is 1, and the stepper stops on the return Err(…) line. The next stop shows treat at line 1, which is not a line of treat: the debugger is in code that the compiler wrote for ?. Then the stepper stops on the closing brace of treat; the println! line never ran. Back in main, the match takes the Err arm, and the run prints failed: need 2, have 1. The panel does not show the Result itself. After the edit, both takes succeed, after_second shows 0, and the run prints both patients treated, then left: 0.
Expected output
failed: need 2, have 102 / Values
What does a failure look like as a value?
Writing a match after every call is long. The ? operator does the same work in one character. After a call that returns a Result, ? gives the value inside Ok, or returns the Err from the current function at once. That is propagating the error: passing it on to your own caller.
? works only inside a function that itself returns a Result. In a main that returns nothing, the compiler reports error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`).
fn treat(left: u32) -> Result<u32, String> {
let after_first = take(left, 2)?;
let after_second = take(after_first, 2)?;
println!("both patients treated");
Ok(after_second)
} ? passes an Err on to the caller in one character, and the return type shows that it can.
Precisely: ? and error types
? also converts the error with the From trait when the function’s error type differs from the call’s. A later lesson on Rust Result, ?, and panic covers this.
03 / Exceptions
What happens when a failure jumps up the call stack?
Rust has one failure that jumps: a panic. It stops the current function and every caller in the thread, dropping their values on the way, and prints a message such as thread 'main' panicked at …. Rust uses panics for bugs, such as an index past the end of a Vec, not for failures that a caller should expect. Ordinary code does not catch them.
let kits = vec![5, 2];
let i = 2;
// panics: index out of bounds: the len is 2 but the index is 2
println!("{}", kits[i]); In Rust, a failure that a caller should expect is a Result; a panic is for bugs.
Precisely: catching a panic
std::panic::catch_unwind can stop a panic, but a build can also be set to abort at once instead (panic = "abort"), and then nothing is caught. Do not use panics for failures you plan to handle.
04 / Costs
Where can each way hide a failure?
Each way has a cost. A Result in the signature tells every reader that the call can fail, and the compiler warns when a Result is not used. (The stepper shows no Rust warnings.)
But two short forms silence it. let _ = take(&mut kit, 4); throws the Result away, without the warning. take(&mut kit, 4).unwrap() takes the value from Ok and panics on an Err, so an expected failure becomes a crash.
Languages with exceptions have the opposite costs. An exception that nobody catches stops the program with a message, so it cannot be dropped without a word. But the signature of a function does not say that it can throw.
// The Err is dropped: no warning, no message.
let _ = take(&mut kit, 4);
// The Err becomes a panic.
take(&mut kit, 4).unwrap(); A Result is visible in the signature but can be thrown away with let _ or turned into a crash with unwrap; an exception cannot be dropped silently but is not visible in the signature.
Coming from Go: let _ = is Rust’s version of Go’s _ for an error: legal, short, and silent.
Experiment
Throw a Result away with let _
take returned an Err. let _ = drops the Result without looking at it, and without the warning that the compiler gives for an unused Result. unwrap does look at it, and turns the Err into a panic.
Why it matters let _ = and unwrap both decide what a failure means without handling it: one ignores it, the other stops the program. Handle it with match, or pass it on with ?.
Predict what is printed. Then change let _ = take(&mut kit, 4); to take(&mut kit, 4).unwrap(); and predict again.
fn take(kit: &mut u32, wanted: u32) -> Result<(), String> {
if wanted > *kit {
return Err(format!("need {}, have {}", wanted, kit));
}
*kit = *kit - wanted;
Ok(())
}
fn main() {
let mut kit = 3;
let _ = take(&mut kit, 4);
println!("treated; {} plasters left", kit);
}
What to look for
Step into take: kit shows an address, then → 3, the kit in main, and wanted is 4, so take stops on the return Err(…) line. Back in main, kit is still 3, and the run prints treated; 3 plasters left, although nobody was treated. After the edit, the run stops with an error and prints nothing: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "need 4, have 3"', main.rs:11:23.
Expected output
treated; 3 plasters left05 / Converting
How do you turn one kind of failure into the other?
Rust has a method for each direction. unwrap and expect turn an Err into a panic; expect adds your own message. ok() turns a Result into an Option and drops the error. ok_or turns an Option into a Result by adding the error. A match turns a Result into anything you like, such as a message.
Choose by who must handle the failure. Return the Result where a caller can act on it. Panic, with expect and a message that says why it cannot happen, only where an Err would mean a bug.
// Some(1), or None with the reason dropped
let rest: Option<u32> = take(3, 2).ok();
// Ok(1), or Err with a new reason
let sure: Result<u32, String> =
rest.ok_or(String::from("kit short"));
// 1, or a panic with this message
let one = take(5, 4).expect("kit was counted"); expect turns an Err into a panic, ok() drops the reason, and ok_or adds one.
06 / Try it
Can you explain a fresh example?
Predict every line of output, then compare with the answer. The next lesson, Stack and heap, shows where values are stored while a program runs. Its link is at the end of the page.
fn take(left: u32, wanted: u32) -> Result<u32, String> {
if wanted > left {
return Err(format!("need {}, have {}", wanted, left));
}
Ok(left - wanted)
}
fn treat(left: u32) -> Result<u32, String> {
println!("start");
let rest = take(left, 4)?;
println!("done");
Ok(rest)
}
fn main() {
println!("{:?}", treat(3));
}
What is printed, line by line?
Reveal the reasoning
start, then Err("need 4, have 3"). treat prints start and calls take, which gives an Err. The ? returns that Err from treat at once, so done is never printed. main prints the Result with {:?}, which shows the alternative and the String in quotes.
Practice tickets
- ERR-101 · An unreadable weight gives a dose of 0 mg → Free
- ERR-102 · One unreadable drink hides the rest of the fluid chart → Sign in · Pro
- ERR-103 · Medicine packs vanish when a ward cabinet is full → Sign in · Pro