Lesson 11 / 15
Panic and Unrecoverable Errors
Modeling side by side a design where a failure stops the remaining body without leaving the caller any choice, and a design where the same failure is met with a return value, and comparing the counts the two designs give for the remaining body.
Published Updated
In this lesson
- Compares by executing a recoverable and an unrecoverable design how many times the remaining body runs
- Explains how the same distinction can be modeled without actually triggering a panic
- Distinguishes with an example that choosing panic is a design decision, not an error kind
Contents
The Go Fundamentals course actually triggered a panic and caught it with a recovery
call, printing a result in the form panic=true / panic=false — the panic made a
stack trace visible, while the recovery itself made only a flag visible. That course
could use this catching mechanism because there the panic was triggered at the boundary
of a single call, in a controlled way. This lesson measures the same distinction without
ever triggering it: a real panic is neither set up here nor caught with a recovery call.
Panic text is environment-dependent data — the file name, line number, and the shape of
the call stack vary with the machine it runs on — and this lesson does not print such
text.
Instead, the distinction is modeled: the same failure is built once in a design met
with Result, and once in a design written in the form “nothing after this point runs,”
and the counts the two designs give for the remaining body are compared. The Custom
Error Types lesson showed that a failure can be carried as a value, and that the caller
decides for itself what to do with that value. This lesson measures a design where that
decision is refused: a failure that leaves the caller no choice.
Every mechanism measured in this topic’s previous four lessons — absence, failure arms,
the shorthand, conversion — shared one common assumption: failure was a value, and the
program kept running along with that value. This last lesson looks for the limit of that
assumption: can every failure be handled this way, or is the “continue” option itself
meaningless for some situations? An unrecoverable situation is, by definition, one where
an assumption too fundamental to return as a Result and leave to the caller has
collapsed — and instead of separating these two kinds from each other, this lesson
counts the effect both have on the remaining body.
Two Designs, the Same Condition
The two words in the lesson title — panic and unrecoverable error — are two names for the same idea. “Panic” is how Rust’s runtime names this state; “unrecoverable error” is the same state’s conceptual name, not specific to Rust alone. This lesson treats both under the same measurement: a failure stopping the remaining work without leaving the caller the option to continue.
The two runs below handle the same invalid input (-5) with two different designs. In
the first, the failure is met with a Result, and the caller continues its remaining
work even after seeing the error.
// recoverable.rs — the error is a value, the caller chooses to continue
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;
fn record(ledger: &Ledger, name: &str) {
*ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
*ledger.borrow().get(name).unwrap_or(&0)
}
fn validate(input: i32) -> Result<i32, ()> {
if input < 0 {
return Err(());
}
Ok(input)
}
fn remaining_work(ledger: &Ledger) {
record(ledger, "recoverable-continue");
}
fn main() {
let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
match validate(-5) {
Ok(value) => println!("valid value: {}", value),
Err(()) => println!("invalid input met, program continues"),
}
remaining_work(&ledger);
println!();
println!("recoverable-continue: {} times", times(&ledger, "recoverable-continue"));
}
invalid input met, program continues recoverable-continue: 1 times
EH13. validate(-5) returns an Err, but this return does not stop the program:
match runs the Err arm, then the flow reaches remaining_work in the ordinary way
and runs it. recoverable-continue climbs to one. The caller had an option — seeing the
error and continuing — and it used that option.
In this design, validate reports its failure with exactly the mechanism measured in
this topic’s previous four lessons: it returns Result<i32, ()>, the Err arm carries
no value (()), but its presence is enough. main’s body meets this value with a
match, and in both arms the program reaches the same place, remaining_work. Calling
remaining_work is not a result of the match — it is written after the match
finishes, as an ordinary next line. This is a difference from the pattern seen in
previous lessons: there, the arms did independent jobs; here, both arms lead to the same
next step, only printing a different message.
The second design handles the same condition without ever offering the caller this option.
// unrecoverable.rs — when the condition fails, the remaining body is never called by design
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;
fn record(ledger: &Ledger, name: &str) {
*ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}
fn times(ledger: &Ledger, name: &str) -> u32 {
*ledger.borrow().get(name).unwrap_or(&0)
}
fn continue_work(ledger: &Ledger) {
record(ledger, "unrecoverable-continue");
}
fn main() {
let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
let input = -5;
if input >= 0 {
println!("valid value: {}", input);
continue_work(&ledger);
} else {
println!("unrecoverable condition: the remaining body is never called by design");
}
println!("unrecoverable-continue: {} times", times(&ledger, "unrecoverable-continue"));
}
unrecoverable condition: the remaining body is never called by design unrecoverable-continue: 0 times
EH14. The same condition (input < 0) this time results in continue_work never
being called. unrecoverable-continue stays at zero — written in the source, but
running never in this run. Putting the two runs side by side makes the difference
clear: the same invalid input leads to the remaining work running in one design and
never running in the other. The count comes from the design itself — not from how
“invalid” the input is.
This second design has no Result at all — main itself decides with if/else and
calls continue_work only in the valid branch. The else arm itself prints a message,
but this message is not an error value, only an informational note; the program itself
ends normally after this message. This lesson presents this second design as a model
of a real unrecoverable situation — a real unrecoverable situation typically stops the
program without even waiting for main to finish; here main completes normally, only
the continue_work call ends up skipped. The model carries the “remaining work does not
run” outcome; it does not carry when and how the program itself terminates.
Neither of these two programs shows a real panic; both are written with ordinary
if/match branching, and both terminate normally. The model carries not the panic
state’s actual mechanism (unwinding the call stack) but its observable outcome (the
remaining body never running). A real panic produces this “never runs” outcome
automatically, in a single call, without stopping every function in the intermediate
layers one by one — this lesson does not build that automation, it only compares its
outcome with two hand-written designs.
Why this modeling is necessary comes from the ban on environment-dependent data. When a real panic is triggered, the runtime produces a text; that text includes which file and line it stopped at, and the call stack’s shape at that moment. All of this information depends on the build environment, the file path, and even the compiler’s version — the same source code does not produce the same text when compiled and run on a different machine. The rule kept throughout this course (no printing of real paths, real memory addresses, version-dependent output) applies here too; this is why the panic state is not really triggered, and instead only the part that can be counted — whether the remaining body runs — is modeled.
This modeling belongs to the same family as the rule measured in the previous four lessons that “code the compiler rejects is not placed in a code block.” There, the compiler’s rejection was described in prose, not placed in a code block; here, the panic state itself is never set up at all, and its outcome is modeled instead. What both have in common is putting a number that can be read from the source in place of environment-dependent output.
A Limit on the Measurement
The difference between the two designs above does not say which one is “correct.” Panic is not a kind of error, it is a design decision — the same failure can be written in either form, and the choice depends on what is left to the caller.
EH15. The recoverable design leaves the caller an option: ignoring the error, using a fallback, or producing its own error. The design modeled as unrecoverable closes off that option from the start — the remaining work never runs by design unless the condition is met, and changing that is not in the caller’s hands. A real panic takes this second design one step further: the stop happens not only in one function’s body but in every layer that calls that function, without any explicit code being written. This lesson does not exercise this multi-layered stop; it only shows, at a single layer, how the “never runs” outcome can be modeled.
This difference can be compared to the silent skipping measured in the Shorthand for
Error Propagation lesson, but it is not the same thing. There, early return produced a
Result; the remaining body did not run, but the function returned normally, and the
caller could meet that return with a match. In this design modeled as unrecoverable,
the function also returns normally — the model is not a real panic — but a real panic
differs at exactly this point: it produces no normal return, it leaves the caller no
value to meet with a match. This is the real dividing line between recoverable and
unrecoverable: one leaves a value, the other cuts the flow without leaving any value at
all.
In the design modeled as unrecoverable, continue_work was written as a single body,
but in a real program the remaining work could consist of several steps — opening a
record, computing a total, printing a result. This lesson gathered these steps into a
single function, but the multi-step skipping seen in the Shorthand for Error Propagation
lesson applies here too: when the input >= 0 condition is not met, all the steps
inside continue_work are skipped together, not one by one. How many separate steps
the remaining work consists of does not matter — as long as the condition is not met,
none of them run, all of them together fall into the “never” class.
The moment a designer has to choose between these two paths is usually decided by the answer to “can this situation actually happen, and if it does, does the rest of the program stay trustworthy.” A user’s input being invalid is an expected situation — the recoverable design is the natural choice here, because the caller can anticipate this situation in advance and keep a response ready. By contrast, a situation that breaks a program’s own internal consistency — an array carrying fewer elements than expected, a setup left incomplete — is usually treated as unrecoverable, because in that situation the assumptions the rest of the code was written under are no longer valid, and continuing could lead to new, unpredictable errors.
Drawing a precise line between these two extremes is not this lesson’s job — the line
depends on the program’s own domain and the designer’s decision. This lesson only makes
the outcome of that decision countable: whichever design is chosen, whether the
remaining body runs can be verified not by reading the source, but by measuring a run.
The 1/0 difference between recoverable-continue and unrecoverable-continue is the
last example of the measurement carried throughout this topic: how many times a body
written in the source runs in a run.
Whether a design ends up recoverable or unrecoverable does not follow automatically from
the failure’s nature — it is a decision. The same invalid input can be handled in one
program as “log the error, continue with a default value”; in another program, the same
input can be handled as “this should never have happened, continuing means nothing.”
Rust itself does not impose this decision; both Result and an ordinary function return
are part of the same language, and the choice belongs to whoever writes the source code.
The -5 input to the validate function above actually represents the “same” failure
in both designs — in both, the source says a negative number is considered invalid. The
difference is not in detecting the failure, but in what happens after it is
detected. This stands in the same place as the axis carried throughout this topic: how
many times a body written in the source runs in a run. Only this time the body being
measured is not a fallback value or a conversion, but the rest of a function’s normal
flow — and whether that rest runs is read directly from the design itself.
Summary
- The same failure can be written with two designs: in the recoverable design, the caller continues the remaining work by its own choice; in the design modeled as unrecoverable, the remaining body runs never.
- A real panic was never triggered in this lesson; the distinction was modeled with ordinary branching, without printing panic text — because panic text carries environment-dependent data like the file, line, and call stack.
- In the recoverable design the remaining work ran (
recoverable-continue: 1), in the design modeled as unrecoverable it did not (unrecoverable-continue: 0) — the same condition, a different count. - Panic is not a kind of error but a design decision; the choice depends on what is left to the caller — on whether a situation is expected or breaks the program’s internal consistency.
- A real panic produces the “never runs” outcome in a single call, without stopping the intermediate layers one by one; this lesson measured not that automation, only its observable outcome.
- In the recoverable design, the caller meets a value and decides; in the unrecoverable design, no value is left at all, the flow is cut directly.
Next Step
The bodies measured throughout this topic’s five lessons were always inside a function’s own body — how much of it ran when a function was called: a fallback value, an arm, a conversion, a remaining body. All five lessons settled into the same three classes (every time, only when needed, never), only which mechanism fell into these classes changed. The Closures lesson carries the same question to when a body is called: does a closure run the moment it is defined, or only when someone calls it? The “how many times did it run” question measured in this topic continues there too, but now what is measured is no longer a failure — it is a definition itself.
To keep your progress and take notes, Log in
My notes
Log in to take notes.