Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close