Display, and a real error typeLesson 0005 · after 0003 · reading, then a 20-minute drill on your own code · ~30 minutes
Every code block, every output line, and every error message on this page was produced by running it. Nothing here is written from memory.
The demo domain is a temperature sensor, deliberately — your project is a task CLI, so nothing below can be pasted into it. You have to translate, and translating is where the learning happens.
You passed all 17 tests. The three modules — task, command, store — do what the spec asked, you used ? with ok_or, and find(|task| task.id == id) is the idiomatic answer, not a loop. Structs, enums and the two-crate package are no longer the weak spot.
But the tests only reach the library. main.rs is the one file no test could see, and it is the one file that misses the contract. Run your own crate:
$ cargo run -- fly
no valid commands
$ echo $?
0
$ cargo run -- done 9
thread 'main' (146760) panicked at src/main.rs:25:33:
called `Result::unwrap()` on an `Err` value: "id not found"
$ echo $?
101
The spec said: errors print to stderr and exit with status 1. What happens instead is three separate faults:
println! sends errors to stdout, so a pipe or a redirect mixes them into real output..unwrap() on complete() and remove() panics — status 101 and a backtrace hint — on the ordinary, expected case of a wrong id.That is not a syntax gap. It is the exact thing your mission names: errors with Result, not panic!. And the tidy fix needs one thing you have not met yet: traits. So — traits first, then you fix those three faults yourself in the drill at the bottom.
A trait is a list of method signatures that a type can promise to provide. Nothing more. If you have used an interface, you have the shape already; the differences come later.
trait Reading {
fn celsius(&self) -> f64;
fn label(&self) -> String {
format!("{:.1}C", self.celsius())
}
}
Two kinds of method are in there, and the difference is the whole of Part 1:
celsius ends in a semicolon — a required method. Every implementor must write it.label has a body — a default method. Implementors get it for free and may override it. Note that the default calls self.celsius(), a method the trait does not yet have an implementation for. That is allowed: the trait can build on its own promises.Now two unrelated types keep that promise. impl Trait for Type:
struct Thermometer { room: String, celsius: f64 }
struct Kettle { fahrenheit: f64 }
impl Reading for Thermometer {
fn celsius(&self) -> f64 { self.celsius }
fn label(&self) -> String { // overrides the default
format!("{} is {:.1}C", self.room, self.celsius)
}
}
impl Reading for Kettle {
fn celsius(&self) -> f64 { // takes the default `label`
(self.fahrenheit - 32.0) * 5.0 / 9.0
}
}
1. kitchen is 21.5C
2. 100.0C
Line 1 is the override, line 2 is the default method doing the work for a type whose numbers were never even in Celsius. Two types, one vocabulary.
impl Type block is what a type can do for itself. An impl Trait for Type block is a type keeping a promise someone else defined. Same keyword, two different jobs — that is why 0004's impl Task { .. } and this page's impl Reading for Kettle { .. } look so similar.
Break the promise — drop celsius from the Kettle impl — and the compiler names exactly what is missing:
error[E0046]: not all trait items implemented, missing: `celsius`
--> src/main.rs:31:1
|
5 | fn celsius(&self) -> f64;
| ------------------------- `celsius` from trait
...
31 | impl Reading for Kettle {
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation
Book: 10.2 Defining a Trait · Default Implementations
Display: the trait behind {}Here is the part that pays off immediately. println!("{}", x) is not magic and it is not built into the language for "printable things". It calls one trait method, Display::fmt. A type prints with {} if — and only if — someone implemented that trait for it.
use std::fmt;
impl fmt::Display for Thermometer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} [{:.1}C]", self.room, self.celsius)
}
}
Three things in that signature to notice, then never think about again:
f that the caller supplied — no allocation happens for a println!.write! is format! aimed at a destination. It has the same syntax and returns the fmt::Result you need, which is why the body is one line with no semicolon.fmt::Result is just Result<(), fmt::Error> under a different name.3. kitchen [21.5C]
4. to_string gave: kitchen [21.5C]
Line 4 is the bonus and it is worth understanding: .to_string() was never written for Thermometer. The standard library says every type implementing Display gets ToString automatically. One trait implemented, a second one granted. (std: ToString — "implemented automatically for any type which implements Display")
And the type without the impl — Kettle — cannot use {} at all:
error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
--> src/main.rs:63:23
|
63 | println!("5. {}", k);
| -- ^ `Kettle` cannot be formatted with the default formatter
|
help: the trait `std::fmt::Display` is not implemented for `Kettle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
You have met this error already, in 0003 — that is what #[derive(Debug)] and {:?} were for. Now the split is clear:
Debug — {:?} | Display — {} | |
|---|---|---|
| Audience | You, debugging | The user of the program |
| How you get it | #[derive(Debug)] | Hand-written; no derive exists |
| Shape | Structure: Task { id: 1, .. } | Whatever you decide it reads like |
There is no #[derive(Display)] on purpose: the compiler can print your fields mechanically, but only you know how the sentence should read.
Book: 10.2 Implementing a Trait on a Type · std: fmt::Display
You may write impl SomeTrait for SomeType only if the trait or the type is yours. Display for your Task: fine, the type is yours. Your own trait for Vec<T>: fine, the trait is yours. Display for Vec<String>: rejected — both belong to the standard library. This is the orphan rule, and it exists so no other crate can change what your code already does. (Book 10.2, "coherence")
A generic <T> on its own means "any type at all" — and a function that accepts any type may do almost nothing with it, because the compiler has no idea what it can do. Watch it fail:
fn hottest<T>(items: &[T]) -> Option<&T> {
items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
}
error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
--> src/main.rs:46:34
|
46 | items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
| ^^^^^^^ method not found in `&&T`
|
= help: items from traits can only be used if the trait is implemented and in scope
note: `Reading` defines an item `celsius`, perhaps you need to implement it
The fix is a bound — a promise demanded of the caller's type:
fn hottest<T: Reading>(items: &[T]) -> Option<&T> {
items.iter().max_by(|a, b| a.celsius().total_cmp(&b.celsius()))
}
5. hottest: attic [28.0C]
Read <T: Reading> as: T can be any type, as long as it implements Reading. Inside the function you may now use every method the trait promises, and nothing else. Both sides get a guarantee, both checked at compile time, and no lookup happens at runtime.
Three spellings of the same idea, so you recognise all of them in other people's code:
fn show<T: Reading>(r: &T) // bound in the angle brackets
fn show(r: &impl Reading) // same thing, shorter
fn show<T>(r: &T) where T: Reading // same thing, for long bound lists
Book: 10.2 Traits as Parameters · 10.1 Generic Data Types
Your tasks crate reports failures as String. That works and 0003 asked for it deliberately, because the real answer needs Part 1 to Part 3. Here it is.
Start with what you already know how to write — an enum, one variant per way of failing, carrying whatever the caller needs:
#[derive(Debug)]
enum SensorError {
Empty,
NotANumber(ParseFloatError),
OutOfRange(f64),
}
Compare that with String. A caller can match on this and react differently per case; it cannot match on prose. The bad reading is still in the value, so the message can be built later, at the edge of the program. And you cannot typo a variant — "out of rnage" compiles, OutOfRnage does not.
Then two traits turn it from "an enum" into "an error":
impl fmt::Display for SensorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SensorError::Empty => write!(f, "no reading given"),
SensorError::NotANumber(e) => write!(f, "not a number: {}", e),
SensorError::OutOfRange(v) => write!(f, "{} is outside -90..60", v),
}
}
}
impl Error for SensorError {} // std::error::Error
Display is the human sentence — Part 2, applied. Error is an empty impl: it adds no code, it only marks the type as an error so it fits everywhere the ecosystem expects one. But it does demand something. Delete the Display impl and keep impl Error:
error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
--> src/main.rs:13:16
|
13 | impl Error for SensorError {}
| ^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `std::fmt::Display` is not implemented for `SensorError`
Error requires Display and Debug — a trait can demand other traits, the same way a function demands bounds. That is why #[derive(Debug)] sits on the enum. (std: Error — "Errors must describe themselves through the Display and Debug traits")
From: a trait you have been using since chapter 1Yes — From is another trait, and it lives in the standard library. Its whole definition is one required method:
trait From<T> {
fn from(value: T) -> Self; // build a Self out of a T
}
You have called it in every lesson so far without knowing it had a name:
let s = String::from("hi"); // this IS From: impl From<&str> for String, in std
So read the impl below as an English sentence — "here is how to build a SensorError out of a ParseFloatError":
impl From<ParseFloatError> for SensorError {
fn from(e: ParseFloatError) -> SensorError {
SensorError::NotANumber(e)
}
}
It is an ordinary function with a wrapper around it. You can call it by hand, and nothing magic happens:
let e: ParseFloatError = "nope".parse::<f64>().unwrap_err();
let wrapped: SensorError = SensorError::from(e); // just a function call
Because ? calls it for you. This is the whole point. ? does not simply hand the error to your caller — it converts it first:
let value = thing()?;
// what the compiler writes for you:
let value = match thing() {
Ok(v) => v,
Err(e) => return Err(From::from(e)), // <- YOUR impl runs here
};
These three functions are therefore the same function. Same output, three spellings:
fn by_hand(text: &str) -> Result<f64, SensorError> {
match text.parse::<f64>() {
Ok(v) => Ok(v),
Err(e) => Err(SensorError::from(e)), // call it yourself
}
}
fn with_into(text: &str) -> Result<f64, SensorError> {
match text.parse::<f64>() {
Ok(v) => Ok(v),
Err(e) => Err(e.into()), // `.into()` is From from the other side
}
}
fn with_question(text: &str) -> Result<f64, SensorError> {
Ok(text.parse::<f64>()?) // `?` calls it for you
}
1. Err(NotANumber(ParseFloatError { kind: Invalid }))
2. Err(NotANumber(ParseFloatError { kind: Invalid }))
3. Err(NotANumber(ParseFloatError { kind: Invalid }))
e.into() and SensorError::from(e) are the same trait read in opposite directions: from starts from the destination type, into starts from the value you hold. Implement From and you get into for free — you never write an Into impl.
Delete the impl From and the compiler names precisely what is absent:
error[E0277]: `?` couldn't convert the error to `SensorError`
--> src/main.rs:28:27
|
27 | fn with_question(text: &str) -> Result<f64, SensorError> {
| ------------------------ expected `SensorError` because of this
28 | Ok(text.parse::<f64>()?)
| --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`
| |
| this can't be annotated with `?` because it has type `Result<_, ParseFloatError>`
"the trait From<X> is not implemented for YourError" always means the same thing: write the recipe from X to YourError. (Write the same code with an annotated let value: f64 = text.parse()?; and the report arrives as E0271 instead, pointing at the same missing impl.)
command.rs, line 13fn parse(args: &[String]) -> Result<Command, String> {
let first = args.first().ok_or("No Arguments Found")?;
// ^^^^^^^^^^^^^^^^^^ this is a &str, not a String
}
ok_or("No Arguments Found") produces Result<_, &str>, and your function promises Result<_, String>. Two different types — the same mismatch as above. It compiled because the standard library already ships impl From<&str> for String, and ? found it. Verified:
$ cargo run
Err("No Arguments Found") // a String, converted on the way out
That is why the mechanism was invisible in 0003: std had written the impl you needed. The moment your own error type appears, you write it.
And that is what keeps a deep call stack readable — every layer writes a bare ?, and each error type carries its own recipe for becoming the layer above.
The finished function, with all three failure paths and one bare ? doing the conversion:
fn parse_reading(text: &str) -> Result<f64, SensorError> {
if text.is_empty() {
return Err(SensorError::Empty);
}
let value: f64 = text.parse()?; // ParseFloatError becomes SensorError here
if value < -90.0 || value > 60.0 {
return Err(SensorError::OutOfRange(value));
}
Ok(value)
}
1. Ok(21.5)
2. not a number: invalid float literal
3. 900 is outside -90..60
4. no reading given
Book: 9.2 The ? operator · std: From
One trait object is worth knowing before you touch your CLI. Box<dyn Error> means "some value on the heap that implements Error, decided at runtime" — the escape hatch when a function can fail in unrelated ways and you do not want an enum listing them all. main may return it:
fn main() -> Result<(), Box<dyn Error>> {
let ok = parse_reading("18.25")?;
println!("5. ? gave us {}", ok);
let boom = parse_reading("nope")?; // fails here
println!("never printed {}", boom);
Ok(())
}
5. ? gave us 18.25
Error: NotANumber(ParseFloatError { kind: Invalid })
$ echo $?
1
The exit status is right, and ? in main is genuinely useful in a script or a test binary. But look at the message: NotANumber(ParseFloatError { kind: Invalid }). That is Debug, not your carefully written Display — main's reporting uses {:?}. For a CLI a human runs, you want your own sentence, so you handle it yourself at the top:
match parse_reading(text) {
Ok(v) => println!("reading {:.1}C", v),
Err(e) => {
eprintln!("error: {}", e); // stderr, and Display
process::exit(1);
}
}
$ cargo run -- 21.5
reading 21.5C
$ echo $?
0
$ cargo run -- warm
error: not a number: warm
$ echo $?
1
eprintln! is println! aimed at stderr; process::exit(1) sets the status a caller reads. Those two lines and the missing ? are the entirety of what your main.rs is short of.
Answer from memory. Scrolling up to check first is the one way to waste these. Some questions are about older topics on purpose — mixing them is what makes any of it stick.
Traits
In a trait definition, what is the difference between a method that ends with a semicolon and one that ends with a block?
Display
You wrote impl fmt::Display for Task. Which of these does that also give you, with no extra code?
Error handling
Your function returns Result<T, MyError> and calls something that fails with io::Error. You want a bare ? to work. What must you write?
Generics
Why does fn longest<T>(a: &T, b: &T) refuse to compare a and b with >, and what is the smallest fix?
Ownership
In fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result, why is the first parameter &self rather than self?
Enums
Name two concrete advantages an error enum has over a String error, as your tasks crate uses today.
Modules & paths
You add impl fmt::Display for Task in src/task.rs. What does main.rs have to import to print a task with {}?
tasks unchanged. Keep the syntax reference open — looking syntax up is free, copying answers is not.
Work in ~/learn-rust/tasks. Three steps, each with its own check. Run cargo test at the end: all 17 must still pass, because you are not changing the library's contract.
Display for TaskYour main.rs builds the list line by hand inside a closure. Move that decision to the type: implement fmt::Display for Task in src/task.rs, producing exactly the format the spec prints — 1 [todo] buy milk (medium) — then reduce the list arm to printing each task with {}.
Check: cargo run -- add x still works, and list's output format has not changed.
The file needs use std::fmt; at the top. The impl block goes anywhere in task.rs, and the one method is fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result. The body is a single write!(f, ..) with no semicolon; inside it you can call self.status.label() just as main.rs does now.
unwrap, no panicMove the body of main into a second function that returns Result<(), String>, so Command::parse, complete and remove can all be reached with ? instead of unwrap and nested matches. main keeps only: collect the args, call it, and deal with the error. While you are there, remove prints nothing today — make it say removed <id>.
Check: grep unwrap src/main.rs finds nothing.
fn run(args: &[String], store: &mut Store) -> Result<(), String>. Its first line can be match Command::parse(args)? { .. } — the ? lands on parse, so the match arms deal with Command values, not Results. Every arm ends in (), and the function's last line is Ok(()). This works with no From impl because every error in play is already String.
In main, report the failure on stderr with your own message and exit with status 1. Nothing else changes.
Check — all three must hold:
$ cargo run --quiet -- fly ; echo $?
error: no valid commands
1
$ cargo run --quiet -- done 9 ; echo $?
error: id not found
1
$ cargo run --quiet -- add "buy milk" 2>/dev/null ; echo $?
added task 1
0
use std::process; at the top. Then if let Err(e) = run(&args, &mut store) { .. } is enough — inside it, eprintln!("error: {}", e); followed by process::exit(1);. The third check passes automatically once errors leave stdout: redirecting stderr to /dev/null must not swallow real output.
Converting String errors into a proper TaskError enum with Display, Error and From is the obvious next move, and it is deliberately not in this drill — it touches all four files and it is the next lesson. Get these three green first.
impl Trait for Type is a type keeping that promise.{} is Display and nothing else — you write it by hand, and to_string() comes free with it. {:?} is Debug, which you derive.<T> can do nothing; <T: Trait> can do exactly what the trait promises.Display plus the empty impl Error; ? converts between error types by calling From.Result, on stderr, with exit 1. unwrap is for the cases you have proved impossible.