513 lines
28 KiB
HTML
513 lines
28 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>0006 — Your own error type</title>
|
||
<link rel="stylesheet" href="../assets/style.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<h1>Your own error type</h1>
|
||
<p class="subtitle">Lesson 0006 · after <a href="0005-traits-display-and-errors.html">0005</a> · reading, then a 25-minute drill against a shipped test file · ~40 minutes</p>
|
||
|
||
<div class="callout">
|
||
Every code block and every compiler message on this page was produced by running it, in a scratch project, today.
|
||
Nothing is written from memory. The demo domain is a config loader — your project is a task CLI, so nothing here
|
||
pastes in. Translating is the work.
|
||
</div>
|
||
|
||
<h2>Where 0005 left you</h2>
|
||
|
||
<p>The drill landed: <code>Display for Task</code>, no <code>unwrap</code>, <code>Command::parse(args)?</code>,
|
||
<code>eprintln!</code> + <code>process::exit(1)</code>, all 17 tests still green. The contract 0003 asked for and never
|
||
got is now real code.</p>
|
||
|
||
<p>So look at what is left. Two lines in two different files:</p>
|
||
|
||
<pre><code>// src/command.rs — the user typed `done` with no id after it
|
||
let id = args.get(1).ok_or("id not found")?;
|
||
|
||
// src/store.rs — the user typed `done 9`, and task 9 does not exist
|
||
Err("id not found".to_string())</code></pre>
|
||
|
||
<p>Two unrelated failures, one identical sentence. A caller cannot tell them apart, and neither can you at 3am.
|
||
There is a third: <code>Err(String::from("no valid commands"))</code> throws away the word the user actually typed,
|
||
so your CLI can never say <em>which</em> command it did not recognise.</p>
|
||
|
||
<p>That is the last thing standing between <code>tasks</code> and a crate you would show an interviewer. Today it
|
||
becomes one enum and three traits.</p>
|
||
|
||
<h2>Part 1 — What a <code>String</code> error costs</h2>
|
||
|
||
<p>Three costs, and they are not stylistic.</p>
|
||
|
||
<table>
|
||
<tr><th>With <code>String</code></th><th>With an enum</th></tr>
|
||
<tr>
|
||
<td>The caller gets prose. To react differently per failure it must <em>match on text</em> — <code>if msg == "id not found"</code> — and that breaks the day you fix a typo.</td>
|
||
<td>The caller matches on a variant. The compiler checks the arms.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>The data is gone. <code>format!("no task with id {id}")</code> flattens the id into text; nothing downstream can use it.</td>
|
||
<td>The value rides along — <code>NotFound(9)</code> — and the sentence is built at the edge, where the human is.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>A misspelled message compiles.</td>
|
||
<td>A misspelled variant does not.</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p>This is not just taste; it is the published guideline for the language:</p>
|
||
|
||
<blockquote>
|
||
<p>"Error types should always implement the <code>std::error::Error</code> trait… Never use <code>()</code> as an error
|
||
type, even where there is no useful additional information for the error to carry… The error message given by the
|
||
<code>Display</code> representation of an error type should be lowercase without trailing punctuation, and typically
|
||
concise."</p>
|
||
</blockquote>
|
||
<p class="cite">Rust API Guidelines: <a href="https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err">C-GOOD-ERR</a></p>
|
||
|
||
<p>Note the lowercase rule — that is why <code>"invalid digit found in string"</code>, straight from
|
||
<code>std</code>, has no capital and no full stop. Your messages will match that style, and the shipped tests check
|
||
it.</p>
|
||
|
||
<h2>Part 2 — The one new trait method: <code>source()</code></h2>
|
||
|
||
<p>0005 gave you the recipe: <code>#[derive(Debug)]</code>, then <code>Display</code>, then the empty
|
||
<code>impl Error</code>, plus <code>From</code> so a bare <code>?</code> converts. That is 90% of today's drill and
|
||
you already have it.</p>
|
||
|
||
<p>Here is the 10% that is new, and it is the part that makes wrapped errors behave. When one of your variants
|
||
carries another error — <code>BadPort(ParseIntError)</code> — you now have two sentences for one failure: yours and
|
||
<code>std</code>'s. The <code>Error</code> trait has a slot for the inner one:</p>
|
||
|
||
<pre><code>pub trait Error: Debug + Display {
|
||
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
|
||
// …plus deprecated description() / cause(), which you never implement
|
||
}</code></pre>
|
||
<p class="cite">std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html">std::error::Error</a> — note the
|
||
supertraits: <code>Debug + Display</code> is a <em>requirement of the trait itself</em>, the same bound idea as
|
||
0005's <code><T: Reading></code>, applied to a trait instead of a function.</p>
|
||
|
||
<p>The rule that goes with it is one sentence, and it is easy to get wrong:</p>
|
||
|
||
<blockquote>
|
||
<p>"In error types that wrap an underlying error, the underlying error should be either returned by the outer error's
|
||
<code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both."</p>
|
||
</blockquote>
|
||
<p class="cite">std: <a href="https://doc.rust-lang.org/std/error/trait.Error.html#error-source">Error source</a></p>
|
||
|
||
<p>So: say your sentence in <code>Display</code>, hand the cause to <code>source()</code>, and never print both.
|
||
Here is the whole pattern in a config loader — <code>Option</code> in, <code>u16</code> out, two ways to fail:</p>
|
||
|
||
<pre><code>#[derive(Debug)]
|
||
enum ConfigError {
|
||
Missing(String),
|
||
BadPort(ParseIntError),
|
||
}
|
||
|
||
impl fmt::Display for ConfigError {
|
||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
match self {
|
||
ConfigError::Missing(key) => write!(f, "missing setting: {}", key),
|
||
// no `{e}` on the next arm - the port text is not worth echoing
|
||
ConfigError::BadPort(_) => write!(f, "port must be a number"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Error for ConfigError {
|
||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||
match self {
|
||
ConfigError::BadPort(e) => Some(e), // the cause lives here instead
|
||
ConfigError::Missing(_) => None, // nothing underneath this one
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<ParseIntError> for ConfigError {
|
||
fn from(e: ParseIntError) -> ConfigError { ConfigError::BadPort(e) }
|
||
}
|
||
|
||
fn port(text: Option<&str>) -> Result<u16, ConfigError> {
|
||
let text = text.ok_or(ConfigError::Missing("port".to_string()))?;
|
||
Ok(text.parse()?) // ParseIntError -> ConfigError, via From
|
||
}</code></pre>
|
||
|
||
<pre><code>1. Ok(8080)
|
||
2. port must be a number
|
||
3. Some("invalid digit found in string")
|
||
4. missing setting: port
|
||
5. None</code></pre>
|
||
|
||
<p>Line 2 is your sentence. Line 3 is <code>std</code>'s, reached through <code>source()</code> — still available for
|
||
a log or a <code>--verbose</code> flag, not shoved in the user's face. Lines 4–5: a variant with nothing underneath
|
||
it returns <code>None</code>, and that is not a gap, it is the answer.</p>
|
||
|
||
<p>Read <code>Option<&(dyn Error + 'static)></code> as "maybe a reference to some error, whatever type it
|
||
is" — the <code>dyn</code> from 0005's <code>Box<dyn Error></code>, borrowed instead of boxed. Copy the
|
||
signature; it is not worth memorising.</p>
|
||
|
||
<h3>What the enum buys the caller</h3>
|
||
|
||
<p>Now the payoff that a <code>String</code> can never give you — recovering from <em>one</em> failure and staying
|
||
fatal on the rest:</p>
|
||
|
||
<pre><code>fn port_or_default(text: Option<&str>) -> Result<u16, ConfigError> {
|
||
match port(text) {
|
||
Err(ConfigError::Missing(_)) => Ok(8080), // recover from ONE variant
|
||
other => other, // every other failure stays fatal
|
||
}
|
||
}</code></pre>
|
||
|
||
<pre><code>6. Ok(8080)
|
||
7. Err("port must be a number")</code></pre>
|
||
|
||
<p>A missing setting falls back to a default; a typo'd one still fails. Try writing that against
|
||
<code>Result<u16, String></code> — you would be comparing prose. This is the shape of every real config loader,
|
||
retry policy, and HTTP status decision you will write in a backend job.</p>
|
||
|
||
<h2>Part 3 — What the compiler starts doing for you</h2>
|
||
|
||
<p>An enum is a <em>closed</em> set, and the compiler knows all of it. Add a variant to a shipped error type:</p>
|
||
|
||
<pre><code>enum TaskError {
|
||
…
|
||
NotFound(u32),
|
||
StoreFull, // new today
|
||
}</code></pre>
|
||
|
||
<pre><code>error[E0004]: non-exhaustive patterns: `&TaskError::StoreFull` not covered
|
||
--> src/error.rs:19:15
|
||
|
|
||
19 | match self {
|
||
| ^^^^ pattern `&TaskError::StoreFull` not covered
|
||
|
|
||
note: `TaskError` defined here
|
||
--> src/error.rs:6:10
|
||
|
|
||
6 | pub enum TaskError {
|
||
| ^^^^^^^^^
|
||
...
|
||
14 | StoreFull,
|
||
| --------- not covered
|
||
= note: the matched value is of type `&TaskError`
|
||
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern
|
||
or an explicit pattern as shown</code></pre>
|
||
|
||
<p>That is the answer to the question you missed in 0005's quiz, delivered by the compiler: <strong>adding a
|
||
failure mode makes the build fail everywhere the new case is unhandled.</strong> With a <code>String</code>, adding
|
||
a failure mode is silent — you find out in production. This is also the argument for <em>not</em> reaching for
|
||
<code>_ => …</code> in a <code>match</code> on your own error type: the wildcard throws the guarantee away.</p>
|
||
|
||
<p class="cite">Book: <a href="https://doc.rust-lang.org/stable/book/ch06-02-match.html">6.2 <code>match</code></a>
|
||
(exhaustiveness) · <a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">9.2 <code>?</code> and <code>From</code></a></p>
|
||
|
||
<h3>The three errors you will meet during the migration</h3>
|
||
|
||
<p>Not hypotheticals — I ran your crate with each mistake in place. Recognise them and each costs you ten seconds
|
||
instead of ten minutes.</p>
|
||
|
||
<p><strong>1. You wrote the new module but never declared it.</strong></p>
|
||
<pre><code>error[E0432]: unresolved import `crate::error`
|
||
--> src/command.rs:1:12
|
||
|
|
||
1 | use crate::error::TaskError;
|
||
| ^^^^^ unresolved import</code></pre>
|
||
<p>A file in <code>src/</code> is not a module until a <code>mod</code> declaration names it. Add
|
||
<code>pub mod error;</code> to <code>src/lib.rs</code>. (Chapter 7, still true.)</p>
|
||
|
||
<p><strong>2. You changed the signature but left an old <code>String</code> behind.</strong></p>
|
||
<pre><code>error[E0308]: mismatched types
|
||
--> src/store.rs:37:13
|
||
|
|
||
37 | Err("id not found".to_string())
|
||
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `TaskError`, found `String`
|
||
| |
|
||
| arguments to this enum variant are incorrect</code></pre>
|
||
<p>This is the migration working as intended: the compiler is listing your remaining <code>String</code> errors one
|
||
at a time. Follow it until it stops.</p>
|
||
|
||
<p><strong>3. You used <code>?</code> on <code>parse()</code> with no <code>From</code> impl.</strong></p>
|
||
<pre><code>error[E0271]: type mismatch resolving `<u32 as FromStr>::Err == TaskError`
|
||
--> src/command.rs:31:43
|
||
|
|
||
31 | Ok(Command::Done { id: id.parse()? })
|
||
| ^^^^^ expected `TaskError`, found `ParseIntError`</code></pre>
|
||
<p>0005 predicted exactly this: when the target type comes from context rather than a turbofish, the missing
|
||
<code>From</code> is reported as <code>E0271</code> instead of <code>E0277</code>. Same meaning, same fix — write
|
||
<code>impl From<ParseIntError> for TaskError</code>.</p>
|
||
|
||
<h2>Retrieval — before the drill</h2>
|
||
|
||
<p>From memory. Scrolling up first is the one way to waste these. Two questions are about older material on
|
||
purpose — interleaving is what makes any of it stick.</p>
|
||
|
||
<div class="q" data-type="recall" data-topic="Enums">
|
||
<p class="topic">Enums</p>
|
||
<p class="prompt">You add a variant to an error enum that is matched in four places. What does the compiler do, and what does the equivalent change to a <code>String</code> error do?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">The build fails at every <code>match</code> that does not cover the new variant — <code>error[E0004]: non-exhaustive patterns</code> — so the compiler hands you the exact list of places to update. Adding a new failure message to a <code>String</code> error changes nothing at compile time; every caller keeps compiling and silently mishandles the new case. This is why <code>_ => …</code> on your own error type is a mistake: it opts out of the guarantee.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="mcq" data-topic="Error handling">
|
||
<p class="topic">Error handling</p>
|
||
<p class="prompt">Your variant <code>BadId(ParseIntError)</code> wraps another error. Where should <code>std</code>'s message appear?</p>
|
||
<div class="options">
|
||
<button class="opt" data-correct="false">In your Display output, and also from source()</button>
|
||
<button class="opt" data-correct="true">From source() only, not in your Display output</button>
|
||
<button class="opt" data-correct="false">In neither, since the outer message replaces it</button>
|
||
</div>
|
||
<div class="explain hidden">std's own words: the underlying error "should be either returned by the outer error's <code>Error::source()</code>, or rendered by the outer error's <code>Display</code> implementation, but not both." Choose <code>source()</code>: the report stays one clean sentence, and a log or a <code>--verbose</code> flag can still dig out the cause.</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Traits">
|
||
<p class="topic">Traits</p>
|
||
<p class="prompt">The trait is declared <code>pub trait Error: Debug + Display</code>. What are those two names doing there, and what happens if your type has neither?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">They are supertraits — bounds on the trait itself: you may only implement <code>Error</code> for a type that already implements <code>Debug</code> and <code>Display</code>. Without them, <code>impl Error for MyError {}</code> fails with <code>E0277: unsatisfied trait bound</code> (0005 showed the real output). It is the same mechanism as <code><T: Reading></code> on a function, pointed at a trait.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Generics">
|
||
<p class="topic">Generics</p>
|
||
<p class="prompt">What does this test actually assert, given that its body is empty? <code>fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}</code> then <code>assert_usable_as_error::<TaskError>();</code></p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">Nothing at runtime — it is a compile-time assertion. Naming <code>TaskError</code> as the type argument forces the compiler to check every bound, so the test fails to build if <code>TaskError</code> stops implementing <code>Error</code>, or stops being <code>Send</code>/<code>Sync</code>. The API guidelines ask for exactly those bounds, because an error that is not <code>Send</code> cannot cross a thread boundary — which every web framework does.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="mcq" data-topic="Ownership">
|
||
<p class="topic">Ownership</p>
|
||
<p class="prompt">Why does <code>UnknownCommand(String)</code> hold an owned <code>String</code> rather than a borrowed <code>&str</code> taken from the argument list?</p>
|
||
<div class="options">
|
||
<button class="opt" data-correct="false">Because a borrowed str cannot be printed by Display</button>
|
||
<button class="opt" data-correct="true">Because the error outlives the args it was built from</button>
|
||
<button class="opt" data-correct="false">Because String is faster to match against than str</button>
|
||
</div>
|
||
<div class="explain hidden">The error is returned upward, out of the function that borrowed the argument slice. A <code>&str</code> in the variant would need a lifetime parameter — <code>TaskError<'a></code> — infecting every signature that mentions it. Owning one short string at the moment of failure is the cheap, boring answer. This is chapter 4 deciding your API shape again, and it is why you have not needed chapter 10.3 yet.</div>
|
||
</div>
|
||
|
||
<div class="q" data-type="recall" data-topic="Modules & paths">
|
||
<p class="topic">Modules & paths</p>
|
||
<p class="prompt">You create <code>src/error.rs</code> and <code>store.rs</code> says <code>use crate::error::TaskError;</code>. It fails with <code>E0432: unresolved import</code>. Why, and what is the fix?</p>
|
||
<button class="reveal-btn">Show answer</button>
|
||
<div class="answer hidden">A file in <code>src/</code> is not part of the crate until something declares it. Add <code>pub mod error;</code> to <code>src/lib.rs</code> (<code>pub</code> because <code>main.rs</code> is a separate crate and needs to name the type too). <code>use</code> only creates a shortcut to a path that already exists — it never brings a file into the module tree.</div>
|
||
<div class="grade hidden">
|
||
<button data-grade="hit">Got it</button>
|
||
<button data-grade="miss">Missed it</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="summary"><div id="summary-body"></div><p id="summary-total"></p>
|
||
<button id="report-btn">Copy report</button><pre id="report-output" class="hidden"></pre></div>
|
||
|
||
<h2>The drill — 25 minutes, your own crate</h2>
|
||
|
||
<p>Type it, do not paste it. The config loader above is a different program; none of it fits
|
||
<code>tasks</code> unchanged. Keep the <a href="../reference/rust-syntax.html#error-types">syntax reference</a> open —
|
||
looking syntax up is free, copying answers is not.</p>
|
||
|
||
<p>This time the feedback loop is a test file, like 0003. Install it first:</p>
|
||
|
||
<pre><code>cd ~/learn-rust/tasks
|
||
cp ../lessons/0006-errors-spec.rs tests/errors.rs
|
||
cargo test # 7 new tests fail to compile — that is the starting line</code></pre>
|
||
|
||
<p>Do not edit <code>tests/errors.rs</code>. Do not edit <code>tests/spec.rs</code> either: all 17 must still pass,
|
||
because the library's <em>behaviour</em> is not changing today — only the type it reports failures with. Target at
|
||
the end: <strong>24 passing</strong>.</p>
|
||
|
||
<h3>Step 1 — <code>src/error.rs</code></h3>
|
||
|
||
<p>New file, new module. The tests name the variants, so this shape is fixed:</p>
|
||
|
||
<pre><code>pub enum TaskError {
|
||
NoCommand, // no arguments at all
|
||
UnknownCommand(String), // the word the user actually typed
|
||
MissingTitle, // `add` with nothing after it
|
||
MissingId, // `done` / `remove` with nothing after it
|
||
BadPriority(String), // the priority word that was not low/medium/high
|
||
BadId(ParseIntError), // `done abc` — wraps std's parse failure
|
||
NotFound(u32), // the id that was not in the store
|
||
}</code></pre>
|
||
|
||
<p>Write the four things it needs: the derive, <code>Display</code>, <code>impl Error</code> with
|
||
<code>source()</code>, and <code>From<ParseIntError></code>. Two derives are required —
|
||
<code>Debug</code> because <code>Error</code> demands it, and <code>PartialEq</code> because the tests compare
|
||
variants with <code>assert_eq!</code>. Miss the second and you get
|
||
<code>error[E0369]: binary operation == cannot be applied to type TaskError</code>.</p>
|
||
|
||
<p>The messages are part of the contract — the tests compare them exactly. Lowercase, no trailing punctuation,
|
||
per C-GOOD-ERR:</p>
|
||
|
||
<table>
|
||
<tr><th>Variant</th><th><code>to_string()</code> must be</th></tr>
|
||
<tr><td><code>NoCommand</code></td><td><code>no command given</code></td></tr>
|
||
<tr><td><code>UnknownCommand("fly")</code></td><td><code>unknown command: fly</code></td></tr>
|
||
<tr><td><code>MissingTitle</code></td><td><code>add needs a title</code></td></tr>
|
||
<tr><td><code>MissingId</code></td><td><code>this command needs a task id</code></td></tr>
|
||
<tr><td><code>BadPriority("urgent")</code></td><td><code>unknown priority: urgent</code></td></tr>
|
||
<tr><td><code>BadId(..)</code></td><td><code>task id must be a number</code></td></tr>
|
||
<tr><td><code>NotFound(9)</code></td><td><code>no task with id 9</code></td></tr>
|
||
</table>
|
||
|
||
<p><strong>Check:</strong> <code>cargo build</code> compiles the library, with <code>pub mod error;</code> added to
|
||
<code>src/lib.rs</code>.</p>
|
||
|
||
<details>
|
||
<summary>Stuck for ten minutes on <code>source()</code>?</summary>
|
||
<p>Imports: <code>use std::error::Error;</code>, <code>use std::fmt;</code>,
|
||
<code>use std::num::ParseIntError;</code>. The method signature is
|
||
<code>fn source(&self) -> Option<&(dyn Error + 'static)></code> — copy it from Part 2, it is not worth
|
||
deriving. Exactly one variant returns
|
||
<code>Some(e)</code>; a <code>_ => None</code> arm is acceptable here because you are matching to find one case,
|
||
not to handle every case.</p>
|
||
</details>
|
||
|
||
<h3>Step 2 — <code>command.rs</code>: <code>Result<Command, TaskError></code></h3>
|
||
|
||
<p>Change the signature, then let the compiler walk you through the five <code>ok_or</code> / <code>Err</code> sites.
|
||
Two of them get better: the <code>_ =></code> arm can now name the word it rejected, and both
|
||
<code>match id.parse() { Ok(n) => n, Err(_) => return Err(..) }</code> blocks collapse to
|
||
<code>id.parse()?</code> — that is what the <code>From</code> impl was for.</p>
|
||
|
||
<p><strong>Check:</strong> <code>cargo test --test errors parse_errors_name_the_exact_failure</code> passes, and
|
||
<code>grep -c "match id.parse" src/command.rs</code> prints <code>0</code>.</p>
|
||
|
||
<details>
|
||
<summary>Stuck on the unknown-command arm?</summary>
|
||
<p>The match arm <code>_ => …</code> discards the value it matched. Bind it instead:
|
||
<code>other => Err(TaskError::UnknownCommand(other.to_string()))</code>. <code>other</code> is a
|
||
<code>&str</code> (you matched on <code>.as_str()</code>), and the variant holds a <code>String</code> — see the
|
||
Ownership question above for why.</p>
|
||
</details>
|
||
|
||
<h3>Step 3 — <code>store.rs</code>: report <em>which</em> id</h3>
|
||
|
||
<p>Both error returns become <code>TaskError::NotFound(id)</code>. Nothing else in the file changes.</p>
|
||
|
||
<p><strong>Check:</strong> <code>cargo test --test errors store_reports_which_id_was_missing</code> passes.</p>
|
||
|
||
<h3>Step 4 — <code>main.rs</code>: the edge</h3>
|
||
|
||
<p><code>run</code> now returns <code>Result<(), TaskError></code>. Everything else you already wrote in 0005
|
||
stays exactly as it is — <code>eprintln!("error: {}", e)</code> keeps working because <code>Display</code> is
|
||
implemented, and that is the whole point of the trait.</p>
|
||
|
||
<p><strong>Check — the observable behaviour of your CLI:</strong></p>
|
||
|
||
<pre><code>$ cargo run --quiet -- fly ; echo $?
|
||
error: unknown command: fly
|
||
1
|
||
$ cargo run --quiet -- done abc ; echo $?
|
||
error: task id must be a number
|
||
1
|
||
$ cargo run --quiet -- done 9 ; echo $?
|
||
error: no task with id 9
|
||
1
|
||
$ cargo run --quiet ; echo $?
|
||
error: no command given
|
||
1
|
||
$ cargo run --quiet -- add "buy milk" 2>/dev/null ; echo $?
|
||
added task 1
|
||
0
|
||
$ cargo test
|
||
… 17 passed … 7 passed …</code></pre>
|
||
|
||
<p>Compare the first three lines with what your CLI said an hour ago — <code>no valid commands</code>,
|
||
<code>id not found</code>, <code>id not found</code>. Same code paths, same exit codes; the errors now name the
|
||
thing that went wrong. That is the whole return on one enum.</p>
|
||
|
||
<h3>Then stop</h3>
|
||
|
||
<p>Persistence is the obvious next move and it is deliberately not here: reading and writing a file brings
|
||
<code>fs</code>, <code>io::Error</code>, a second <code>From</code> impl, and turning a line of text back into a
|
||
<code>Task</code>. That is lesson 0007, and it is much easier once <code>TaskError</code> exists to convert
|
||
<em>into</em>.</p>
|
||
|
||
<h2>What you are missing, measured</h2>
|
||
|
||
<p>You asked not to miss anything important, so I mapped this workspace against the book's real table of contents
|
||
rather than my memory of it: <strong><a href="../reference/book-coverage.html">the coverage map</a></strong>. Read it
|
||
once — it is the shortest honest answer to "where am I?".</p>
|
||
|
||
<p>The summary: chapters 1–7, 9, and 10.2 are <em>produced</em>, not just read. Three genuine gaps stand between you
|
||
and a job-ready floor, in the order I intend to teach them:</p>
|
||
|
||
<ol>
|
||
<li><strong>ch 12 — files and <code>io::Error</code></strong>: your CLI forgets everything on exit (lesson 0007).</li>
|
||
<li><strong>ch 8 + 13 — <code>HashMap</code>, <code>map</code>/<code>filter</code>/<code>collect</code></strong>: your
|
||
weakest measured area, and the most common shape in real Rust code.</li>
|
||
<li><strong>ch 11 — writing tests</strong>: you have consumed 24 of my tests and written none. A take-home will ask
|
||
you to produce them.</li>
|
||
</ol>
|
||
|
||
<p>Lifetimes (ch 10.3) are untouched, and that is fine for now — you have dodged them by owning your data, which is
|
||
the right call in a CLI. They become unavoidable when you read other people's code.</p>
|
||
|
||
<h2>Take it outside</h2>
|
||
|
||
<p>Once the 24 tests are green, <code>tasks</code> is a small, complete, idiomatic crate — the first thing in this
|
||
workspace worth showing to strangers. The highest-value thing you can do with it is ask people who write Rust daily
|
||
whether your error type is idiomatic:</p>
|
||
|
||
<ul>
|
||
<li><a href="https://users.rust-lang.org">users.rust-lang.org</a> — the official forum, "Code Review" category.
|
||
Paste <code>error.rs</code> and ask specifically: is one enum for both parse and store failures right, or should
|
||
those be two types with a wrapping variant? That is a genuine design question with a real answer, and it is the kind
|
||
of thing a reviewer will teach you in one reply.</li>
|
||
<li><a href="https://reddit.com/r/rust">r/rust</a> — faster, noisier; good for "is this idiomatic?" checks.</li>
|
||
</ul>
|
||
|
||
<p>Two things you will likely hear back, and both are worth knowing in advance: real crates often reach for
|
||
<a href="https://docs.rs/thiserror"><code>thiserror</code></a> to derive exactly the <code>Display</code>/<code>From</code>
|
||
code you just wrote by hand, and applications often use
|
||
<a href="https://docs.rs/anyhow"><code>anyhow</code></a> instead of an enum. Both are correct advice, and both are
|
||
the wrong place to start — you cannot judge a macro that writes an <code>Error</code> impl until you have written
|
||
one yourself. Today's version is the one that teaches; reach for the crates on your second real project.</p>
|
||
|
||
<h2>The five sentences worth keeping</h2>
|
||
|
||
<ol>
|
||
<li>An error type is an enum with one variant per way of failing, carrying the data that failed.</li>
|
||
<li><code>Display</code> is the sentence a human reads: lowercase, no trailing punctuation, no repeat of the cause.</li>
|
||
<li><code>source()</code> is where a wrapped error goes — either <code>source()</code> or <code>Display</code>, never both.</li>
|
||
<li><code>From<Cause> for MyError</code> is what makes a bare <code>?</code> convert; missing, it reports as
|
||
<code>E0277</code> or <code>E0271</code> depending on how the target type was named.</li>
|
||
<li>The compiler's exhaustiveness check is the real reason for an enum: adding a failure mode breaks the build,
|
||
not production.</li>
|
||
</ol>
|
||
|
||
<footer>
|
||
<p><strong>Primary source:</strong> re-read
|
||
<a href="https://doc.rust-lang.org/stable/book/ch09-02-recoverable-errors-with-result.html">The Rust Book 9.2 —
|
||
Recoverable Errors with <code>Result</code></a>, now that you have written the type it describes; then the two-screen
|
||
<a href="https://doc.rust-lang.org/std/error/trait.Error.html">std::error::Error</a> page, which is the definition of
|
||
what an error <em>is</em>. If you read one third thing, read
|
||
<a href="https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err">C-GOOD-ERR</a>
|
||
— it is the checklist an experienced reviewer applies to your error type.</p>
|
||
<p>Previous: <a href="0005-traits-display-and-errors.html">0005 — Traits, Display, errors</a> ·
|
||
<a href="0004-structs-enums-packages.html">0004 — Structs, enums, packages</a> ·
|
||
<a href="0003-build-a-task-cli.html">0003 — Task CLI project</a><br />
|
||
Reference: <a href="../reference/rust-syntax.html">Rust syntax reference</a> ·
|
||
<a href="../reference/book-coverage.html">Coverage map</a></p>
|
||
<p><strong>Ask me things.</strong> Bring me the compiler output verbatim — especially in step 2, where the error type
|
||
changes under five call sites at once. If a paragraph did not land, name it; vague explanation is my fault, not
|
||
yours, and it is far cheaper to fix here than in the middle of the drill.</p>
|
||
</footer>
|
||
|
||
<script src="../assets/quiz.js"></script>
|
||
</body>
|
||
</html>
|