The Tyranny of 'Fail Fast'
In most modern programming languages, from Java to Python to C++, error handling follows a familiar script. A function deep in the call stack encounters a problem—a file is missing, a network connection drops, a value is invalid. It “throws an exception.”
Code higher up the stack can “catch” this exception and try to deal with it. This model forces a stark choice. When an error is thrown, the stack unwinds, effectively blowing up that entire path of computation. The catcher, often far removed from the source of the problem, has limited options: log the error and crash, or log the error and return a default value, pretending nothing went wrong. The low-level code that actually knows what happened has no say in the matter; its only job is to light the fuse. This decouples the detection of a problem from the policy for solving it, but in a very destructive way.
Lisp’s Radical Idea: Errors as a Negotiation
Long before `try-catch` became standard, Common Lisp introduced a far more sophisticated mechanism: the condition system. It’s more than just error handling; it's a way for different parts of a program to communicate about exceptional situations without immediately resorting to a computational panic attack. The core idea is to separate three distinct responsibilities: signaling a condition, handling it, and restarting. Instead of throwing a destructive exception, a Lisp function “signals” a condition. A condition is just an object that describes a situation—it doesn't have to be an error, it could just be a warning or something noteworthy. This signal travels up the stack, looking for a handler. Crucially, finding a handler does not automatically unwind the stack. The program pauses, and a conversation begins.
Meet Conditions and Restarts
This is where the magic happens. The code that signals the condition can also provide a menu of options for how to proceed. These options are called “restarts.” A restart is a function that tells the program how to resume execution from the point where the condition was signaled. The handler, which lives higher up in the call stack, can examine the condition object and then choose one of the available restarts. It’s a two-way dialogue. The low-level code says, “Houston, we have a problem. Here are three ways we can try to fix it and continue.” The high-level code, which has the broader strategic context, can then reply, “Okay, thanks for the options. Let’s go with option two.” This is fundamentally different from exception handling, which is a one-way street ending in a cliff.
A Powerful, Practical Example
Imagine you're writing a program to parse a large log file. A low-level function, `parse-log-entry`, is responsible for reading one line at a time. What happens if it finds a malformed entry? A Java or Python function would likely throw a `MalformedDataException`. The high-level loop processing the file would catch it, log the error, and skip to the next line. That’s the only real choice. In Lisp, `parse-log-entry` could instead signal a `malformed-log-entry` condition and offer several restarts. For example: `skip-entry`, `retry-parse-with-different-rules`, or even `prompt-user-for-correction`. A simple script might have a handler that just invokes the `skip-entry` restart automatically. But a more complex, interactive data-cleaning tool could present those options to a human user, who could then fix the data on the fly. The low-level parsing function doesn't need to know anything about the user interface; it only needs to know how to present its local recovery options. This makes for incredibly robust and flexible software.













