Files
learn-rust/lessons/0006-your-own-error-type.html
T

513 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt; { ... }
// …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>&lt;T: Reading&gt;</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(&amp;self, f: &amp;mut fmt::Formatter&lt;'_&gt;) -&gt; fmt::Result {
match self {
ConfigError::Missing(key) =&gt; write!(f, "missing setting: {}", key),
// no `{e}` on the next arm - the port text is not worth echoing
ConfigError::BadPort(_) =&gt; write!(f, "port must be a number"),
}
}
}
impl Error for ConfigError {
fn source(&amp;self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt; {
match self {
ConfigError::BadPort(e) =&gt; Some(e), // the cause lives here instead
ConfigError::Missing(_) =&gt; None, // nothing underneath this one
}
}
}
impl From&lt;ParseIntError&gt; for ConfigError {
fn from(e: ParseIntError) -&gt; ConfigError { ConfigError::BadPort(e) }
}
fn port(text: Option&lt;&amp;str&gt;) -&gt; Result&lt;u16, ConfigError&gt; {
let text = text.ok_or(ConfigError::Missing("port".to_string()))?;
Ok(text.parse()?) // ParseIntError -&gt; 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&lt;&amp;(dyn Error + 'static)&gt;</code> as "maybe a reference to some error, whatever type it
is" — the <code>dyn</code> from 0005's <code>Box&lt;dyn Error&gt;</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&lt;&amp;str&gt;) -&gt; Result&lt;u16, ConfigError&gt; {
match port(text) {
Err(ConfigError::Missing(_)) =&gt; Ok(8080), // recover from ONE variant
other =&gt; 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&lt;u16, String&gt;</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: `&amp;TaskError::StoreFull` not covered
--&gt; src/error.rs:19:15
|
19 | match self {
| ^^^^ pattern `&amp;TaskError::StoreFull` not covered
|
note: `TaskError` defined here
--&gt; src/error.rs:6:10
|
6 | pub enum TaskError {
| ^^^^^^^^^
...
14 | StoreFull,
| --------- not covered
= note: the matched value is of type `&amp;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>_ =&gt; …</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`
--&gt; 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
--&gt; 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 `&lt;u32 as FromStr&gt;::Err == TaskError`
--&gt; 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&lt;ParseIntError&gt; 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>_ =&gt; …</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>&lt;T: Reading&gt;</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&lt;E: Error + Send + Sync + 'static&gt;() {}</code> then <code>assert_usable_as_error::&lt;TaskError&gt;();</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>&amp;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>&amp;str</code> in the variant would need a lifetime parameter — <code>TaskError&lt;'a&gt;</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 &amp; paths">
<p class="topic">Modules &amp; 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&lt;ParseIntError&gt;</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(&amp;self) -&gt; Option&lt;&amp;(dyn Error + 'static)&gt;</code> — copy it from Part 2, it is not worth
deriving. Exactly one variant returns
<code>Some(e)</code>; a <code>_ =&gt; 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&lt;Command, TaskError&gt;</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>_ =&gt;</code> arm can now name the word it rejected, and both
<code>match id.parse() { Ok(n) =&gt; n, Err(_) =&gt; 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>_ =&gt; …</code> discards the value it matched. Bind it instead:
<code>other =&gt; Err(TaskError::UnknownCommand(other.to_string()))</code>. <code>other</code> is a
<code>&amp;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&lt;(), TaskError&gt;</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&gt;/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&lt;Cause&gt; 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>