12.5When to panic and when to return Result
You now have both tools. panic! for unrecoverable failures (lesson 12.1), Result for recoverable ones (lesson 12.2). The remaining skill is judgment: given a particular failure, which should it be? This lesson gives you the guidelines that experienced Rust programmers use, because choosing well is what makes the difference between a program that fails gracefully and one that crashes in a user's face.
The core question: expected or exceptional?
The single most useful test is this: is this failure something a correct program should anticipate, or is it a sign that something is already broken?
If the failure is a normal, expected part of operation, return a Result. Bad user input, a missing file, a network hiccup, a number that doesn't parse: these happen during the ordinary life of a working program. The caller can and should decide how to respond, so hand them a Result and let them. Panicking over expected failures, as lesson 12.4 showed with the guessing game, turns routine events into crashes.
If the failure means an assumption your code is built on has been violated, panic. An index past the end of an array you just sized, a value the type system should have made impossible, an invariant your own code is supposed to maintain but didn't: these are bugs. Continuing past a bug risks corrupting data or producing wrong answers silently, and a panic stops it cleanly and loudly (lesson 1.7's "fail at compile time or fail loud" philosophy, applied at runtime). A Result here would just be asking the caller to handle your bug, which they can't.
Key insight
Return Result when the caller is in a better position than you to decide what to do about the failure. Panic when there's nothing reasonable to decide, because the failure means the program's assumptions are already broken. "Could a well-written caller recover from this?" If yes, give them a Result. If no, panic.
Guideline: validate at the boundary, trust within
A practical pattern follows from the test above. Data from outside your program (user input, files, network) is untrusted: validate it, with Result, at the boundary where it enters. Once it's validated, the rest of your code can trust it, and code that operates on already-validated data may reasonably panic if its assumptions break, because by then a violation is a bug, not bad input.
This is why the guessing game's read_guess returns a Result (lesson 12.4): it's at the boundary, handling untrusted typing. But a function deep inside a program that receives an already-validated 1..=100 value could assert! that invariant and panic if it's somehow false, because at that point a value outside the range means an earlier bug, not a user mistake.
Guideline: contracts
A function often has a contract: conditions the caller must meet for the call to make sense. "The divisor must not be zero." "The index must be in bounds." When a contract is violated, that's the caller's bug, and panicking is the standard response, because a Result would imply the violation is a normal outcome to handle rather than a mistake to fix. The standard library does exactly this: indexing a vector out of bounds panics, it doesn't return a Result, because a valid program never does it. Document the contract, assert! it if you like (lesson 12.1), and panic when it's broken.
The contrast: if the "bad" condition is something callers can't always prevent (a file that might or might not exist), it isn't a contract violation, it's an expected outcome, so it's a Result.
Guideline: library code leans toward Result
Who's calling matters. In application code, the program you control end to end, you can sometimes panic on a failure you've decided you don't want to handle, because you're the only one affected. In library code, code other people call, strongly prefer Result. A library that panics takes the decision away from its users; it crashes their program over a situation they might have wanted to recover from. Returning Result hands control back to the caller, who knows their context and you don't. As a rule, the more reusable the code, the more it should return errors rather than panic.
When unwrap and expect are fine
Given all this, when is the unwrap/expect panic-shortcut acceptable? A few clear cases:
Examples, prototypes, and exploratory code, where robust error handling would obscure the point and a crash is a fine way to learn something failed. Tests, where a panic is how a test reports failure (chapter 14), so unwrap in a test is idiomatic, not sloppy. And cases where you have information the compiler lacks: if you just wrote "42".parse::<i32>() with a literal you know is valid, unwrap is reasonable because Err genuinely cannot happen. In that last case, prefer expect with a message explaining why it can't fail, so the next reader (and the panic, if you're wrong) understands your reasoning.
Best practice
In code that should keep running, default to Result and reserve panics for broken invariants. Use unwrap/expect freely in tests and quick experiments, and in production only where Err is truly impossible, documented with expect("reason it can't fail"). When in doubt, return a Result: it's easy for a caller to unwrap a Result they don't want to handle, but impossible for them to recover from a panic you chose for them.
Quiz time
Question #1
For each, choose panic or Result, and say why:
a) A function that reads a config file that may not exist. b) A function that's documented to require a non-empty slice and indexes element 0. c) Parsing a command-line argument the user provided.
Show solution
a) Result: a missing file is expected and the caller may want to fall back to defaults. b) Panic: the non-empty requirement is a contract; an empty slice is the caller's bug, and indexing [0] panics anyway. c) Result: user-provided input is untrusted and bad arguments are expected, so validate at the boundary and let the program report the problem.
Question #2
Why should library code lean toward Result even for failures the author might be tempted to panic on?
Show solution
Because a library doesn't know its callers' context, and a panic crashes the caller's whole program, removing their chance to recover. Returning Result hands the decision back to the caller, who can unwrap it if they truly don't care or handle it if they do. Panicking takes a choice that isn't the library's to make.
Question #3
You write let port: u16 = "8080".parse().expect("hardcoded port is valid");. Is expect justified here? What does the message accomplish?
Show solution
Yes: the string is a literal you know parses, so Err is genuinely impossible, which is one of the legitimate unwrap/expect cases. The message documents why you believe it can't fail, so a future reader understands the reasoning, and if the literal is later changed to something invalid, the panic explains the broken assumption instead of just dumping a parse error.
You've been using String as the error type in these examples, which is fine for small programs but limited: a String error can't be inspected, only printed. The last lesson of the chapter shows the sturdier approach, custom error types built from the enums of chapter 11, plus the Box<dyn Error> escape hatch for when you just want errors to flow.