TL;DR
Learn Rust error handling with the Result type, panic!, recoverable vs unrecoverable errors, and the ? operator for propagation
Key concepts
- Rust error handling
- Rust Result type
- Rust panic
- Rust recoverable errors
- Rust unwrap
Error Handling in Rust
Rust groups errors into two categories: recoverable errors (like file not found) and unrecoverable errors (like accessing an array out of bounds). Instead of exceptions, Rust uses Result<T, E> for recoverable errors and panic! for unrecoverable ones.
What You'll Learn
By the end of this lesson you will build a record parser that rejects a malformed line and says which field was wrong — one error type covering three unrelated failures, and a function that chains three fallible steps without handling any of them itself. The part that catches people is the difference between a failure that is reported and one that is skipped: both compile, both run clean, and one of them silently returns a shorter answer than you asked for.
This is the capability the capstone's taskwork is built on — it reads a file of tasks written by hand, so a bad line is expected input rather than a bug, and every message it prints about one comes from an error type designed here. You arrive here able to return an Option when a value might be absent and to model a record as a struct (Option & Result, Structs & Enums). What is new is that the caller now needs to know why something failed, which is precisely what Option cannot tell it.
Unrecoverable Errors with panic!
When something goes terribly wrong and there's no way to recover:
fn main() {
// This will panic and stop the program
panic!("crash and burn!");
}
There is no Run button on that one, and the reason is the point: a panic tears the thread down rather than returning from it. Run it and stderr gets thread 'main' (2030249) panicked at src/main.rs:3:5: — the number in parentheses is the operating system's thread id, so yours will differ and it changes every run — then crash and burn! on the next line, followed by the note: run with RUST_BACKTRACE=1 environment variable to display a backtrace hint. By default Rust then unwinds: it walks back up the stack running each destructor along the way, and once it reaches the top of main the process exits with code 101. (You can opt into panic = "abort" in a release profile to skip the unwinding and stop immediately, but that is a build setting, not the default.) Either way there is no return value and nothing for the caller to inspect — that is exactly what makes it unrecoverable.
Here is one you can run, and then break on purpose. As written it prints both lines and exits 0:
fn main() {
let v = vec![1, 2, 3];
let index = 1;
println!("about to read element {} of a {}-element vector", index, v.len());
// Swap this 1 for 99 and this line becomes the last one that runs.
let picked = v[index];
println!("got {}", picked);
}
Now make it fail: change let index = 1; to let index = 99; and Run it again. The first println! still appears and the second never does — a panic stops the program at the point it happens, not retroactively, so everything already printed stays printed and nothing after it will ever run. On the lane behind that button the failure reads:
thread 'main' (576) panicked at /tmp/main.rs:8:19:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Same three-part shape as the panic! above — thread id in parentheses that differs every run, the message, and the backtrace hint — and the same exit code, 101. The message here is unusually informative for a panic, index out of bounds: the len is 3 but the index is 99, because the Index implementation for a slice knows both numbers and can say them. Most panics tell you far less, as the collections lesson's no entry found for key will show. What no panic gives you is a way to carry on: there is no value to return and nothing for the caller to inspect.
The Result Enum
For recoverable errors, Rust uses the Result type:
enum Result<T, E> {
Ok(T), // Success, contains the value
Err(E), // Error, contains error info
}
Before you look for a Run button. That block is a declaration, not a program — it shows you the shape of
Resultas the standard library defines it, and there is nothing in it to execute. The sandbox behind the Run button compiles a whole crate withrustc --edition 2021, so a file containing only anenumfails withE0601: main function not found in crate. Nothing is wrong with the code; it simply has no entry point. You do not need to declare this type yourself either —Resultis in the prelude, already generic over your success typeTand your error typeE. The runnable examples start in the next section, whereResultvalues actually come back from a call.
Basic Usage
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
let file = match file_result {
Ok(f) => f,
Err(e) => {
println!("Failed to open file: {}", e);
return;
}
};
println!("File opened successfully!");
}
Handling Different Error Types
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let file = File::open("hello.txt");
let file = match file {
Ok(f) => f,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("File not found, creating it...");
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Couldn't create file: {:?}", e),
}
}
other_error => {
panic!("Problem opening file: {:?}", other_error);
}
},
};
}
Shortcuts: unwrap and expect
For prototyping or when you're sure an operation will succeed:
use std::fs::File;
fn main() {
// Make the operation genuinely certain to succeed before we shortcut it.
File::create("hello.txt").expect("Failed to create hello.txt");
// expect: panics with YOUR message if Err. Here it succeeds, so it just
// hands back the File.
let file = File::open("hello.txt")
.expect("Failed to open hello.txt");
println!("opened: {:?}", file.metadata().unwrap().is_file());
// unwrap: same thing with a generic panic message instead of your own.
let _again = File::open("hello.txt").unwrap();
println!("unwrap worked too");
// Uncomment to see the failure side: this file does not exist, so expect
// panics with "Failed to open missing.txt: Os { code: 2, ... }".
// let _missing = File::open("missing.txt").expect("Failed to open missing.txt");
}
Propagating Errors
Often you want to return errors to the caller. The next two fences are the same function written twice — first in full, then with everything the compiler can supply removed — so read them as a pair rather than as two examples.
Here is the long form, annotated. Every decision in it is spelled out, because the short form below is going to delete most of them and you want to know what was deleted:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let file = File::open("username.txt");
// The match is here to UNWRAP, not to handle: the Ok arm takes the file
// out and the Err arm hands the same error straight back to the caller.
// Note the `return` — that arm exits the whole function, not the match.
let mut file = match file {
Ok(f) => f,
Err(e) => return Err(e),
};
let mut username = String::new();
// Same shape again, with one difference: this match is the function's
// last expression, so its arms ARE the return value and neither needs
// `return`. Ok(_) discards the byte count and returns what was filled in.
match file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
fn main() {
match read_username_from_file() {
Ok(name) => println!("Username: {}", name),
Err(e) => println!("Error reading username: {}", e),
}
}
The ? Operator
The ? operator provides a concise way to propagate errors — and read_username_from_file below is the same function as the one above, with the two matches replaced by two question marks.
This fence carries no notes, on purpose. Before you read past it, do the comparison yourself: point at each ? and say which of the two matches above it replaced, and say what became of the Err(e) arm that used to be written out. Then answer the harder one — what property of this particular function makes a bare ? legal at all?
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
// Even more concise with chaining:
fn read_username_short() -> Result<String, io::Error> {
let mut username = String::new();
File::open("username.txt")?.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
match read_username_from_file() {
Ok(name) => println!("Username: {}", name),
Err(e) => println!("Error: {}", e),
}
}
That last question is the answer worth having, so here it is. Both of those functions get away with a bare ? for one reason: every operation they call fails with io::Error, which is exactly what the function returns — so there is nothing to convert, and ? reduces to "unwrap on Ok, return early on Err", which is precisely what the two matches above spelled out by hand. The moment the error types differ, ? needs something extra — and what it needs is worth seeing explicitly. The program below returns its own ConfigError, but the ? sits on a parse() that produces a ParseIntError. There is a println! inside the From impl so you can see precisely when the conversion happens.
Predict
parse_port returns Result<i64, ConfigError>, but the ? is applied to parse(), which fails with ParseIntError — a completely different type. The From impl prints a line when it runs. What are the four printed lines, in order?
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
NotANumber(String),
OutOfRange(i64),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::NotANumber(msg) => write!(f, "not a number ({})", msg),
ConfigError::OutOfRange(n) => write!(f, "{} is outside 1..=65535", n),
}
}
}
// This impl is the ONLY thing that lets ? cross from one error type to another.
impl From<ParseIntError> for ConfigError {
fn from(e: ParseIntError) -> Self {
println!(" [From ran]");
ConfigError::NotANumber(e.to_string())
}
}
fn parse_port(raw: &str) -> Result<i64, ConfigError> {
// parse() produces ParseIntError, but this function returns ConfigError.
let n: i64 = raw.trim().parse()?;
if !(1..=65535).contains(&n) {
return Err(ConfigError::OutOfRange(n));
}
Ok(n)
}
fn main() {
match parse_port("8080") {
Ok(port) => println!("a: port {}", port),
Err(e) => println!("a: error: {}", e),
}
match parse_port("http") {
Ok(port) => println!("b: port {}", port),
Err(e) => println!("b: error: {}", e),
}
match parse_port("99999") {
Ok(port) => println!("c: port {}", port),
Err(e) => println!("c: error: {}", e),
}
}The lines are a: port 8080, then [From ran], then b: error: not a number (invalid digit found in string), then c: error: 99999 is outside 1..=65535. ? is not merely an early return — it is an early return with a conversion. It desugars to roughly match expr { Ok(v) => v, Err(e) => return Err(From::from(e)) }, so the function's error type only has to implement From of whatever error the expression produced. That is why one custom error enum plus a few From impls lets every fallible step in a function use a bare ?. Two details worth keeping: the conversion runs only on the failing path (call a never touches From), and an Err you construct yourself at the right type — like the OutOfRange case — bypasses it entirely.
Assembling a ? Chain
A function whose whole job is to chain fallible steps has a shape worth being able to write from memory. Here are the error type and the three helpers the next block's lines call, so you are ordering against real signatures rather than described ones. Read them for their return types: all three hand back a Result whose error is the same RecordError, and that single fact is what the block is really about.
use std::fmt;
#[derive(Debug)]
enum RecordError {
MissingField(&'static str),
BadNumber(String),
OutOfRange(u32),
}
impl fmt::Display for RecordError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RecordError::MissingField(name) => write!(f, "missing field: {}", name),
RecordError::BadNumber(raw) => write!(f, "not a number: '{}'", raw),
RecordError::OutOfRange(n) => write!(f, "{} is outside 0..=100", n),
}
}
}
fn split_record(record: &str) -> Result<(&str, &str), RecordError> {
record.split_once(':').ok_or(RecordError::MissingField("score"))
}
fn parse_score(raw: &str) -> Result<u32, RecordError> {
let trimmed = raw.trim();
trimmed
.parse::<u32>()
.map_err(|_| RecordError::BadNumber(trimmed.to_string()))
}
fn check_range(score: u32) -> Result<u32, RecordError> {
if score > 100 {
Err(RecordError::OutOfRange(score))
} else {
Ok(score)
}
}
Assemble the body of parse_record, which is declared fn parse_record(record: &str) -> Result<(String, u32), RecordError>:
Arrange the code
These four lines are the entire body of parse_record: split a record into a name and a raw score, turn the raw text into a number, check that number is in range, and hand back the pair. Every step can fail and none of them is handled here. The pieces are shuffled. Put them in the order that runs and returns Ok with the name and score — then answer what the order is really testing: what does each line need that the line above it produces, and why can the last line not move at all?
let score = parse_score(raw)?;let checked = check_range(score)?;Ok((name.trim().to_string(), checked))let (name, raw) = split_record(record)?;
Using ? with Option
The ? operator also works with Option:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
let text = "Hello\nWorld";
match last_char_of_first_line(text) {
Some(c) => println!("Last char: {}", c),
None => println!("No character found"),
}
}
Creating Custom Errors
You can define your own error types:
use std::fmt;
#[derive(Debug)]
struct ValidationError {
message: String,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error: {}", self.message)
}
}
fn validate_age(age: i32) -> Result<(), ValidationError> {
if age < 0 {
Err(ValidationError {
message: String::from("Age cannot be negative"),
})
} else if age > 150 {
Err(ValidationError {
message: String::from("Age seems unrealistic"),
})
} else {
Ok(())
}
}
fn main() {
match validate_age(25) {
Ok(()) => println!("Age is valid"),
Err(e) => println!("{}", e),
}
match validate_age(-5) {
Ok(()) => println!("Age is valid"),
Err(e) => println!("{}", e),
}
}
When to Use panic! vs Result
Use panic! when:
- The error is unrecoverable (bug in code, corrupted data)
- In examples, prototypes, or tests
- When you're absolutely sure the code won't fail
Use Result when:
- The error is expected and recoverable
- The caller should decide how to handle it
- In library code (let users decide)
That table has a third column nobody writes down, and it is the one that causes production incidents: silently doing neither. Before meeting an example of it, recall the tool that makes it possible.
Recall
Without scrolling up: in Option and Result you met .ok(), which converts a Result into an Option. Recall precisely what it does with the error, and say what that means for a function whose job is to report failures.
.ok() maps Ok(v) to Some(v) and Err(e) to None, and the error value is gone — None carries no payload, so there is nowhere for it to live. That makes .ok() right where a fallback is coming and the reason genuinely does not matter, and wrong anywhere the function's contract is to explain what failed. Its mirror image, ok_or/ok_or_else, converts the other way by supplying an error the Option never had.
Which brings us to the failure mode that is worse than a panic: code that neither panics nor reports. The next program compiles without a warning, returns Ok for both inputs, and looks entirely correct in review. Work out what happened to the bad row before you change anything:
Debug
parse_rows must abort on the first bad row and report it — a partial order is worse than no order. It compiles cleanly and both calls return Ok, but one of them is hiding a rejected row. Say which line discards the failure, and why nothing warns you, then fix it.
#[derive(Debug, PartialEq)]
enum RowError {
BadQuantity(String),
}
/// Parse "name, quantity" rows into an order.
/// The FIRST bad row must abort the parse and be reported to the caller —
/// a partial order that silently drops a line is worse than no order at all.
fn parse_rows(rows: &[&str]) -> Result<Vec<(String, u32)>, RowError> {
let parsed: Vec<(String, u32)> = rows
.iter()
.filter_map(|row| {
let (name, qty_raw) = row.split_once(',')?;
let qty = qty_raw.trim().parse::<u32>().ok()?;
Some((name.trim().to_string(), qty))
})
.collect();
Ok(parsed)
}
fn main() {
let good = ["bolts, 12", "nuts, 4"];
let bad = ["bolts, 12", "washers, many", "nuts, 4"];
println!("good -> {:?}", parse_rows(&good));
println!("bad -> {:?}", parse_rows(&bad));
assert_eq!(
parse_rows(&good),
Ok(vec![("bolts".to_string(), 12), ("nuts".to_string(), 4)]),
"a clean input should parse both rows"
);
assert_eq!(
parse_rows(&bad),
Err(RowError::BadQuantity("many".to_string())),
"a bad quantity must abort the parse and be reported, not be dropped"
);
println!("All checks passed.");
}Expected output: good -> Ok([("bolts", 12), ("nuts", 4)])
bad -> Err(BadQuantity("many"))
All checks passed.
Two lines conspire, and neither one warns. .ok() throws the parse error away, and the ? after it returns early from the closure, not from parse_rows — and filter_map treats a closure returning None as "skip this element", which is exactly what it is designed for. So the bad row vanishes and the function reports Ok with a shorter Vec; the only evidence is that one list is shorter than its input. The fix is structural: a skipping adapter cannot express "abort", so replace the chain with a for loop that uses map_err(...)? on the parse, which propagates out of parse_rows itself. Keep the general form of this: filter_map and .ok() are both "skip the failure" tools, and using them inside a function contracted to report failures yields a plausible, silently truncated answer instead of an error.
Practice Exercise
Reading about ? is not the same as designing an error type it can flow through. This is a build task: a small program that reports its own pass/fail. Four functions are stubbed out — three small fallible steps and one that chains them with ?. Run the starter as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.
Notice what kind of functions these are before you write them, because it decides how you will check them for the rest of the track. All four take their input as arguments and return a value; none reads a file, prints anything, or touches a global. A function shaped like that can be called with any input you like and compared against an expected answer, which is the entire prerequisite for automated testing — and it is why the checks below are written as assert_eq! calls rather than as instructions to look at some output. That battery is a test suite. It is missing only the vocabulary: Rust has a #[test] attribute and a built-in runner that finds every function marked with it, reports pass and fail per test, and keeps running after the first failure instead of stopping. Testing in Rust teaches that machinery in full.
Two reasons not to jump ahead and write #[test] here. The first is practical: the Run button on this page has no test mode, so a #[test] function would be stripped before compilation and silently never run — Testing in Rust states this limitation to you directly, and it is why every build task on this track asserts inside main. The second is that the vocabulary is the easy half. The habit is to build the parts of a program as pure functions in the first place, so that checking them is possible at all; the parts that read files and print reports are the parts you will not be able to test this way. You have that habit from here on: any function you can call twice with different arguments and predict the answer for is one you should be asserting on, whether or not you know the attribute yet.
The RecordError enum and its Display impl are written for you, because the exercise is the plumbing, not the formatting: each of the three steps has to produce a RecordError (ok_or for the missing case, map_err for the parse case, a plain Err for the range case) so that parse_record can chain all three with bare ? and let the first failure fall straight out to the caller.
Build
Finish the build. Four functions are stubbed out and the checks below them fail until each one behaves. Run it as-is to see which check fails first, decide what that function is missing, then implement all four until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — but note TODO 4 is what the checks actually call, so nothing passes until the chain is wired up too.
// A record parser with one error type covering three distinct failures.
use std::fmt;
#[derive(Debug, PartialEq)]
enum RecordError {
MissingField(&'static str),
BadNumber(String),
OutOfRange(u32),
}
impl fmt::Display for RecordError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RecordError::MissingField(name) => write!(f, "missing field: {}", name),
RecordError::BadNumber(raw) => write!(f, "not a number: '{}'", raw),
RecordError::OutOfRange(n) => write!(f, "{} is outside 0..=100", n),
}
}
}
// TODO 1: split record once on ':' into (name, raw_score).
// No colon means the score field is missing:
// Err(RecordError::MissingField("score")).
// Hint: split_once(':') returns an Option<(&str, &str)>; ok_or turns an
// Option into a Result by supplying the error it never had.
fn split_record(record: &str) -> Result<(&str, &str), RecordError> {
let _ = record;
Ok(("", ""))
}
// TODO 2: trim raw and parse it as a u32, converting the parse failure
// into RecordError::BadNumber(<the trimmed text>).
// parse::<u32>() gives a Result whose error is a ParseIntError, while this
// function returns a Result<u32, RecordError> —
// map_err is what bridges the two error types.
fn parse_score(raw: &str) -> Result<u32, RecordError> {
let _ = raw;
Ok(0)
}
// TODO 3: accept a score of 0..=100 and reject anything higher with
// RecordError::OutOfRange(score).
fn check_range(score: u32) -> Result<u32, RecordError> {
Ok(score)
}
// TODO 4: chain the three above with ? so the FIRST failure is returned
// to the caller unchanged, and a success yields (trimmed name, score).
// Every ? here returns a RecordError, which is already this function's
// error type — so no conversion is needed at any of the three steps.
fn parse_record(record: &str) -> Result<(String, u32), RecordError> {
let _ = split_record(record);
let _ = parse_score(record);
let _ = check_range(0);
Ok((String::new(), 0))
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
assert_eq!(parse_record("ana: 91"), Ok(("ana".to_string(), 91)), "a well-formed record should parse");
assert_eq!(
parse_record("ana"),
Err(RecordError::MissingField("score")),
"a record with no colon is a MissingField error"
);
assert_eq!(
parse_record("ana: high"),
Err(RecordError::BadNumber("high".to_string())),
"a non-numeric score is a BadNumber error naming the offending text"
);
assert_eq!(
parse_record("ana: 140"),
Err(RecordError::OutOfRange(140)),
"a score above 100 is OutOfRange"
);
println!("All checks passed.");
println!("ok: {:?}", parse_record("ana: 91"));
println!("missing: {}", parse_record("ana").unwrap_err());
println!("bad: {}", parse_record("ana: high").unwrap_err());
println!("range: {}", parse_record("ana: 140").unwrap_err());
}Expected output: All checks passed.
ok: Ok(("ana", 91))
missing: missing field: score
bad: not a number: 'high'
range: 140 is outside 0..=100
Once it passes, try two variations and predict each before running:
- Delete the
map_errand let?do the conversion. Inparse_score, replace the body withlet n = trimmed.parse::<u32>()?; Ok(n). Predict what happens before running. It does not compile:error[E0277]: ? couldn't convert the error to RecordError, with the note "RecordErrorneeds to implementFrom<ParseIntError>".?will convert error types for you, but only along aFromimpl you have written — and this program has none. Addimpl From<ParseIntError> for RecordError(plususe std::num::ParseIntError;) and the bare?compiles, at the cost of losing the offending text unless the impl keeps it. - Swap one
?for anunwrap. Inparse_record, change the first line tolet (name, raw) = split_record(record).unwrap();. Predict which check breaks and how before running. It compiles, and theMissingFieldcheck does not fail — it crashes:thread 'main' panicked at ... called Result::unwrap() on an Err value: MissingField("score"). The program stops at the first bad record rather than reporting it, which is precisely thepanic!-versus-Resultdistinction from earlier in this lesson, applied by accident.
The Same Boundary, With Exceptions
Converting an error as it crosses a boundary is not a Rust idea; it is what a well-written catch block has always done. JavaScript even standardised the chaining part — new Error(msg, { cause }) since ES2022. The mechanism is shared and one property of it is not:
Transfer
Both functions below convert a low-level failure into their module's own error type as it crosses out of them, and both keep the original. The JavaScript version was run as written: it caught a RecordError whose cause was a TypeError, exactly as intended. Which statement names what genuinely transfers between the two, rather than a resemblance?
// The Rust half, complete and runnable. The JavaScript it is compared with
// converts at the same boundary, and note what its middle function does NOT have:
// function parseScore(raw) {
// const n = Number(raw);
// if (Number.isNaN(n)) {
// throw new RecordError(msg, { cause: new TypeError("Number() gave NaN") });
// }
// return n;
// }
// function parseRecord(line) { // no try, no marker, no signature
// const [name, raw] = line.split(":");
// return [name.trim(), parseScore(raw)];
// }
#[derive(Debug)]
enum RecordError {
MissingField(&'static str),
BadNumber(String),
}
fn split_record(record: &str) -> Result<(&str, &str), RecordError> {
record.split_once(':').ok_or(RecordError::MissingField("score"))
}
// The conversion at the boundary: a ParseIntError becomes OUR error type,
// keeping the offending text so the message can quote it.
fn parse_score(raw: &str) -> Result<u32, RecordError> {
let trimmed = raw.trim();
trimmed
.parse::<u32>()
.map_err(|_| RecordError::BadNumber(trimmed.to_string()))
}
// Every fallible call carries a ?. Omitting one is a compile error, not a passthrough.
fn parse_record(record: &str) -> Result<(String, u32), RecordError> {
let (name, raw) = split_record(record)?;
let score = parse_score(raw)?;
Ok((name.trim().to_string(), score))
}
fn main() {
println!("{:?}", parse_record("ana: 91"));
println!("{:?}", parse_record("ana: high"));
}Reading the E0277 That Variation One Produces
That first variation is worth doing for the message alone, because it is the error you will meet every time you add a new fallible call to an existing ? chain. Here it is in full, from the lane:
error[E0277]: `?` couldn't convert the error to `RecordError`
--> /tmp/main.rs:38:35
|
36 | fn parse_score(raw: &str) -> Result<u32, RecordError> {
| ------------------------ expected `RecordError` because of this
37 | let trimmed = raw.trim();
38 | let n = trimmed.parse::<u32>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `RecordError`
| |
| this can't be annotated with `?` because it has type `Result<_, ParseIntError>`
|
note: `RecordError` needs to implement `From<ParseIntError>`
--> /tmp/main.rs:5:1
|
5 | enum RecordError {
| ^^^^^^^^^^^^^^^^
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
Every part of that has a job, and reading them in the right order turns a wall of text into three facts. error[E0277] is a stable, searchable identifier — it always means an unsatisfied trait bound, and rustc --explain E0277 prints the general write-up. The --> gives the primary location, line 38 column 35, and the ^ marks the exact character that failed: the ? itself, not the call before it. Then read the two labels as a pair, because together they are the whole argument: one says what was expected and where that expectation came from (expected RecordError because of this, attached to the return type on line 36), and the other says what was actually found (this can't be annotated with ? because it has type Result<_, ParseIntError>). Expected here, found there, and the reason they must match named in between — the trait From<ParseIntError> is not implemented for RecordError.
Read the trailing note: lines rather than stopping at the first line. They are doing two different jobs. The first note: has its own --> pointing at a different location from the error — line 5, your enum, thirty-three lines above the failure — because that is where the missing implementation would have to go, and the compiler is telling you the fix is not at the line that failed. The second, the = note: at the end, is the mechanism: the question mark operation (?) implicitly performs a conversion on the error value using the From trait. That single line is the model from earlier in this lesson restated by the compiler, and it is what turns the error from a rejection into an instruction.
One habit worth forming here. rustc's diagnostics are good enough that it is tempting to apply the first repair they mention and move on — and in this case the note's suggestion is genuinely right. But notice it names a design decision, not a mechanical edit: writing impl From<ParseIntError> for RecordError makes every bare ? in the module convert automatically, which is what you want for a module-wide error type and is exactly wrong if you wanted the offending text preserved, since the From impl gets only the ParseIntError and not the string that failed to parse. That is the difference between the two solutions this lesson has now shown you, and the compiler cannot know which you meant. Read a suggestion as a hypothesis about your intent, and check it against what you were trying to do.
Capstone Milestone
Capstone milestone
The capstone's taskwork reads a task file a human typed, so a malformed line is ordinary input rather than a bug: it must name what was wrong with which line and carry on being a program. Build that pipeline now, on the record shape you just parsed. One error type for the whole module, three ways of producing it, and a parse function that chains its steps with bare ? and handles nothing itself.
Hint: Write the enum before the functions, not after. The order matters more than it looks: designing the error type first is what lets every step end in a bare ? instead of accumulating map_err calls, and it is the difference between the two chains this lesson showed you.
- One error enum for the module, with a distinct variant per failure the caller could reasonably act on differently.
- A Display impl so a failure can be printed to a user without Debug formatting.
- Every variant that names offending input carries that input, so the message can quote it.
- The parse function chains its steps with bare ? and contains no match on an error and no unwrap.
- The caller — not the parser — decides whether a bad line aborts the run or is reported and skipped.
Key Takeaways
- Rust has no exceptions - use
Resultandpanic! panic!for unrecoverable errors that crash the programResult<T, E>for recoverable errors withOk(T)orErr(E)- Use
unwrap()andexpect()for quick prototyping - The
?operator propagates errors concisely - Prefer
Resultin library code to give callers control - Create custom error types for domain-specific errors
Proper error handling makes your Rust programs robust and reliable!
Next Steps
Now that you understand error handling, you're ready to learn about collections — the vectors, hash maps, and sets Rust uses to store and manage groups of values. Many of their operations return Option, so the handling patterns you just learned will feel right at home.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.