Rust Fundamentals: Ownership, Errors and Fearless Concurrency · Errors, Collections, Traits and Concurrency
Error Handling with Result and ?
Recoverable errors are values of type Result<T, E>. Handle them with match, propagate them with ?, define your own error enum, and keep panics for real bugs.
What you'll build
A config parser that reads key=value lines, converts numbers, and reports exactly which line was wrong, using Result, the ? operator and a custom error type.
The idea
Rust separates two kinds of failure:
- Unrecoverable bugs →
panic!(the program stops). Indexing out of bounds orunwrap()onNonepanics. - Recoverable errors → return a
Result<T, E>:Ok(value)orErr(error). The caller must decide what to do.
The ? operator is the workhorse: on Ok(v) it gives you v; on Err(e) it returns early from the current function with that error (converting it with From if needed). Functions using ? must themselves return Result (or Option).
For your own errors, define an enum with a variant per failure, implement Display for messages, and implement From<OtherError> so ? converts library errors automatically. Crates like thiserror (for libraries) and anyhow (for applications) remove the boilerplate.
The code
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
MissingEquals { line: usize },
BadNumber { line: usize, source: ParseIntError },
Missing(&'static str),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::MissingEquals { line } => write!(f, "line {line}: expected key=value"),
ConfigError::BadNumber { line, source } => write!(f, "line {line}: not a number ({source})"),
ConfigError::Missing(key) => write!(f, "missing required key `{key}`"),
}
}
}
#[derive(Debug)]
struct Config {
port: u16,
workers: u32,
}
fn parse(text: &str) -> Result<Config, ConfigError> {
let mut port = None;
let mut workers = None;
for (i, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line.split_once('=').ok_or(ConfigError::MissingEquals { line: i + 1 })?;
let number = |v: &str| v.trim().parse::<u32>().map_err(|e| ConfigError::BadNumber { line: i + 1, source: e });
match key.trim() {
"port" => port = Some(number(value)? as u16),
"workers" => workers = Some(number(value)?),
_ => {} // ignore unknown keys
}
}
Ok(Config {
port: port.ok_or(ConfigError::Missing("port"))?,
workers: workers.unwrap_or(4),
})
}
fn main() {
let inputs = [
"# server\nport = 8080\nworkers = 8",
"port = 8080",
"port 8080",
"port = 80\nworkers = many",
"workers = 2",
];
for text in inputs {
match parse(text) {
Ok(cfg) => println!("ok: {cfg:?}"),
Err(e) => println!("error: {e}"),
}
}
}
Output
ok: Config { port: 8080, workers: 8 }
ok: Config { port: 8080, workers: 4 }
error: line 1: expected key=value
error: line 2: not a number (invalid digit found in string)
error: missing required key `port`Run this on your computer — see “Get Started” for setup.
Common mistakes
unwrap()everywhere. It turns every error into a crash; reserve it for tests, prototypes and truly impossible cases (or useexpect("reason")).- Ignoring a
Result. The compiler warns (unused Result); handle or propagate it. - Stringly-typed errors (
Result<T, String>) in libraries; callers can't match on causes. Use an enum. - Losing the cause. Keep the source error (as
BadNumber { source }does) so messages and logs stay useful.
Key points
panic!for bugs;Result<T, E>for expected failures.?unwrapsOkor returns theErrearly.- Custom error enums +
Display+Frommake?ergonomic. thiserror(libraries) andanyhow(apps) reduce boilerplate.
Further reading: Error handling.
Practice questions
What does the ? operator do in Rust?
Answer
Applied to a Result, it evaluates to the value inside Ok, or returns early from the enclosing function with the Err (converted via From into the function's error type). It also works on Option, returning None early.
When should Rust code panic instead of returning a Result?
Answer
When something indicates a bug or a broken invariant the program can't sensibly recover from (e.g. an impossible state), and in tests and quick prototypes. Expected failures like bad input, missing files or network errors should be Results.
Open in the app to mark lessons complete, take notes and track your progress.