Every Result you've written so far has carried an error type that arrived ready-made — io::Error from the standard library, ParseIntError from a parse call, all perfectly good, all borrowed from somewhere else. And all about to run straight into a wall, the moment a single function needs to do two genuinely different things that can fail in two genuinely different ways.
The wall, in concrete terms
Picture a function that does two fallible things back to back: read a file into a string (which can fail with an io::Error — the file might not exist, might not be readable, any of a dozen perfectly ordinary reasons), and then parse what it finds into a number (which fails with an entirely different type, ParseIntError — maybe the file just contains the word "hello"). Now try to write this function's signature honestly, reaching for one of the two error types you already happen to have lying around:
Pick io::Error, and the second ? refuses to compile — there's no built-in bridge from ParseIntError to io::Error, and there shouldn't be; the two types describe completely unrelated kinds of failure, and Rust isn't going to invent a relationship between them that doesn't actually exist. Pick ParseIntError instead, and you'd simply break it from the opposite end. There is no existing type anywhere that can honestly say "this function fails, and here, specifically, are the two unrelated reasons why" — because the only person who could ever write that exact sentence is you. Nobody else knows your function's particular blend of risks. So: you write it.
The idea costs nothing extra — you already own every piece of it
Here's the entire trick: define an enum whose variants name every distinct way this specific function can fail, each one wrapping the original error so that no information is ever thrown away. And — stop and actually notice this — that idea requires not one single new concept. An enum whose variants each carry a different shape of data is precisely the enums lesson, word for word — think Message's four variant shapes, or the SpreadsheetCell trick from the vectors lesson that let one type honestly hold several. The only thing that's changed is what you're using the trick for: not "one type, several shapes of data," but "one error type, several genuinely different reasons one function might fail."
Two pieces, and you already trusted every tool inside both of them before you opened this lesson. The enum names the two risks and keeps the original error riding along inside each one. And describe is a perfectly ordinary method — impl block, &self, all from the method-syntax lesson — that turns whichever variant you're holding into a clear sentence a human could actually read, using a match that binds each variant's inner value exactly the way you learned in the pattern-matching lesson, and format! from the strings lesson to stitch the sentence together. Five ideas you already trust, cooperating to solve a problem that genuinely couldn't be solved any other way — because nobody but you could have written the list of things that specifically go wrong inside read_count_from_file.
Putting it to work
And there it is — the function that, two minutes ago, didn't have an honest type to return, now reads as cleanly as anything else in this course. Each ? still does precisely what you learned last lesson: unwrap the success, or stop and send the failure upward. The only new piece is what comes immediately before each one — .map_err(CountError::Io) and .map_err(CountError::Parse) — which translate "whatever specific error this one step produced" into "the one error type this whole function promised to return," right there on the spot, by hand.
Look closely at what's actually riding inside those calls — not a closure with the |e| ... shape you've started recognizing, but the bare names CountError::Io and CountError::Parse. That's not a typo or a shortcut: a tuple-style variant like Io(io::Error) quietly builds its own tiny constructor function the moment you define it, with exactly the type fn(io::Error) -> CountError. And a plain function is something you can hand directly to anything expecting a closure — precisely the same move behind or_insert_with(Vec::new) from the hash-maps exercise a couple of lessons back. "A constructor is just a function — you can pass it directly" is a small idea that, once you've spotted it once, keeps quietly turning up again.
The honest postscript — and exactly where it's pointing
Production Rust code typically takes this idea two steps further than you just did, and it's worth knowing both of them by name before you meet them properly. First, it implements a trait called Display, so the error can be shown to a human directly inside a format string — the very same captured-identifier trick you've used in every println! since lesson one — instead of needing a special .describe() method that nobody outside this file knows exists. Second, it implements a trait called Error, which functions less like an instruction and more like a membership card: it tells the rest of the Rust ecosystem "yes, this genuinely is an error type, you can treat it like one" — the very thing that lets it slot into Box<dyn Error> from last lesson, travel inside other people's error enums, and move cleanly through code you've never seen.
And here's the genuinely satisfying part — the one this entire detour was quietly building toward. Implementing a third trait, called From, for this exact type would make every single one of those .map_err(...) calls simply dissolve. Bare ?, and the conversion happens automatically — exactly the trick last lesson promised was hiding inside that one character. You don't have the tools to write any of these three yet. But you now know precisely why you'd want them, which — by a wide margin — is the harder half of learning anything well. The tools themselves are the entire subject of the lesson directly ahead of you: traits. And once you've met them properly, there's a genuinely satisfying loop waiting to be closed: come back to this exact function, and watch .map_err(CountError::Io) and .map_err(CountError::Parse) melt away into nothing, replaced by two bare question marks.
One more honest note, so a real codebase doesn't catch you off guard: almost nobody hand-writes all of this in production Rust. A small corner of the ecosystem — a crate called thiserror is the one you'll meet most often — exists specifically to generate exactly this scaffolding from a few short annotations written above your enum. Learning to build it by hand first, the way you just did, is precisely what makes those annotations make sense the instant you meet them — you'll read one and immediately recognize the shape of the five things it just saved you from typing.
Quick exercise
Give CountError a third way to fail: a variant called something like Empty, carrying no data, for the case where the file reads and parses just fine but the number inside it is 0 (or the trimmed text was empty before parsing even got a chance to run). Wire it into read_count_from_file — you'll want a plain if check and a bare return Err(...), since this failure doesn't arrive from a ?-able call — and then see what the compiler does to describe's match. (Hint: you've met that exact compiler error by name before, several lessons ago — and this is precisely the moment it earns its keep, turning "I added a new way to fail and forgot to handle it somewhere" from a runtime mystery into a compile-time, line-and-column to-do list.)
Take a moment and look back at the arc that just closed. Three lessons ago, error handling was the one topic this course admitted most newcomers quietly dread. You can now tell, on sight and with real reasons behind it, whether code should panic or recover; collapse a paragraph of error-propagating match arms into a single keystroke; and — what you just finished — design an honest, specific vocabulary for everything that can go wrong inside code that's entirely your own. That isn't "getting through" error handling. That's actually being good at it.
From here, the course turns a corner — toward the ideas that let you stop repeating yourself at the level of entire functions and types, not just individual lines. First stop: the trick that let the standard library write a single Vec<T> that works for every T, a single Option<T> that works for every T — and that you're about to learn how to wield yourself: generics.