Rust's take on error handling extends the language's philosophy of letting the compiler catch everything it can. In this chapter we look at what [[panic|panic!]], [[result|Result<T, E>]], and the [[question-mark-operator|? operator]] each do, and when to lean on the right tool so production code stays predictable.
Why Error Handling Matters
- Systems-language reality: When you deal with files, networks, or user input, failure is normal, so the failure path must be explicit.
- Ownership integration: To clean up resources even when something goes wrong, the error story needs to live inside the type system.
- Minimize runtime panics: Because
panic!aborts the current thread immediately, recoverable situations should express failure viaResultinstead.
Stopping Execution with [[panic|panic!]]
Use [[panic|panic!]] when continuing past a certain point would be incorrect.
fn main() {
let config = std::env::var("APP_CONFIG")
.unwrap_or_else(|_| panic!("APP_CONFIG must be set"));
println!("config = {}", config);
}
- A
panic!unwinds the stack and terminates the current thread. - It's handy while debugging "this should never happen" states or hunting for failure points in toy projects.
- In long-running apps such as CLIs or servers, avoid overusing it and model recoverable failures with
Result.
Treating Failure as Data with [[result|Result<T, E>]]
[[result|Result]] has the shape enum Result<T, E> { Ok(T), Err(E) }, so success and failure both become values.
fn read_config() -> Result<String, std::io::Error> {
std::fs::read_to_string("./config.txt")
}
fn main() {
match read_config() {
Ok(content) => println!("config: {}", content),
Err(err) => eprintln!("Failed to read file: {}", err),
}
}
Ok(T)stores the success value,Err(E)stores why it failed.- The
Etype lets you describe failures precisely, andmatchorif letkeep control flow explicit. - Returning
Resultforces callers to acknowledge failure up front, so just reading a signature hints at safety expectations.
Helper Methods Worth Knowing
- unwrap, expect: Useful for quick prototypes when you need a
Timmediately. For production, emit meaningful messages or replace them with proper handling. - map, map_err, and_then: Apply functions to the success or error side in a functional style.
Propagating Errors with [[question-mark-operator|?]]
The [[question-mark-operator|? operator]] short-circuits when it sees an Err (or None) and returns early from the current function.
fn load_user(id: u64) -> Result<String, Box<dyn std::error::Error>> {
let path = format!("./users/{}.json", id);
let raw = std::fs::read_to_string(&path)?;
let parsed: serde_json::Value = serde_json::from_str(&raw)?;
Ok(parsed["name"].as_str().unwrap_or("unknown").to_string())
}
?forwards theErrup the call stack, matching the function's return type. When it seesOk, it unwraps the inner value and keeps going.- The current function must also return a [[result|
Result]] or [[option|Option]], and the error type must be compatible with what you propagate. - Using
?removes nestedmatchblocks and keeps the happy path straight.
Choosing the Right Tool
| Situation | Recommended tool | Reason |
|---|---|---|
| Irrecoverable bug, debug guardrail | panic!, unreachable! |
Surface the bug immediately so you can fix it fast |
| File/network operations or other expected failures | Result |
Preserve the failure details and hand control to the caller |
| Short tutorials or prototypes | unwrap, expect |
Keep samples compact, then swap in safer handling later |
| Unifying many error sources | Result<T, Box<dyn Error>> or a custom enum |
Report multiple error kinds through one channel |
When in doubt during the early stages, remember this cheat sheet:
- Learning snippets: allow
unwrap/expectbriefly. - Naturally fallible work: return a
Result. - State that must not continue:
panic!is acceptable.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.
💬 댓글
이 글에 대한 의견을 남겨주세요