100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Error Handling and Conditions

How Common Lisp's condition system goes beyond try/catch, letting programs signal, inspect, and interactively recover from errors using restarts.

Practical LISPIntermediate10 min readJul 10, 2026
Analogies

The Condition System: Beyond Simple Exceptions

Common Lisp does not have a bare exception mechanism; it has a condition system that separates the act of signaling a problem from the act of deciding how to respond to it. When code calls (error "disk full") or (signal 'my-condition), the runtime walks up the stack looking for a handler, but crucially it does this without unwinding the stack first. That means a handler can inspect the exact state where the problem occurred and choose to resume execution there, not just log and abandon the computation the way a thrown exception in most languages forces you to.

🏏

Cricket analogy: It's like a third umpire review: when a decision is signalled as uncertain, play does not immediately stop and restart from scratch — the on-field umpire, TV umpire, and captain all get to look at the same replay before choosing how to proceed, rather than just overturning blindly.

Signaling Conditions: error, warn, and define-condition

Conditions are ordinary CLOS classes defined with define-condition, typically subclassing error, warning, or condition itself. (error 'my-condition :slot value) both creates the instance and signals it, and if no handler intervenes, it drops into the debugger. warn signals a condition of type warning and, if unhandled, prints a message and continues — it never enters the debugger by default. This three-way split (error, warning, plain signal) lets a library author express severity precisely: a malformed but recoverable input can warn, while a corrupted file handle should error.

🏏

Cricket analogy: A no-ball called by the umpire is like warn — play continues with a free hit awarded — whereas a run-out appeal that ends an innings is like error, since it must be adjudicated before the game state can move on.

handler-case vs handler-bind

handler-case is the convenient, exception-like form: it unwinds the stack back to the handler-case form before running its clause, so it's safe and simple but you lose access to the original dynamic state. handler-bind, by contrast, runs its handler function in the exact dynamic environment where the condition was signalled, without unwinding — this is what makes restart invocation possible, because the stack frames that established the restarts are still live. As a rule of thumb, reach for handler-case when you just want to catch-and-recover like a normal exception, and handler-bind when you need to inspect state or invoke a restart before deciding to unwind.

🏏

Cricket analogy: handler-case is like a match referee reviewing the incident only after play has stopped and everyone has walked off, while handler-bind is like reviewing it live on the field with the fielders still in position, able to replay the exact moment.

lisp
(define-condition disk-full-error (error)
  ((path :initarg :path :reader disk-full-path))
  (:report (lambda (c stream)
             (format stream "Disk full while writing to ~a" (disk-full-path c)))))

(defun write-log-entry (path entry)
  (restart-case
      (if (disk-space-low-p path)
          (error 'disk-full-error :path path)
          (append-to-file path entry))
    (use-alternate-path (new-path)
      :report "Write to a different path instead"
      (write-log-entry new-path entry))
    (skip-entry ()
      :report "Skip this log entry"
      nil)))

(handler-bind ((disk-full-error
                 (lambda (c)
                   (declare (ignore c))
                   (invoke-restart 'use-alternate-path "/tmp/overflow.log"))))
  (write-log-entry "/var/log/app.log" "startup complete"))

Restarts: Interactive Recovery Points

A restart, established with restart-case or restart-bind, is a named recovery strategy that a signalling function offers to whoever ends up handling its condition — it does not decide how to recover, only what the available options are. Callers invoke one with invoke-restart, or use the built-ins use-value and store-value for the common pattern of substituting a bad value and retrying. This inversion of control is the condition system's signature idea: the low-level code that detects the problem publishes possible fixes, while the high-level code (or even a human at the interactive debugger) picks which one applies.

🏏

Cricket analogy: It's like a captain offering the bowler several field-placement options after a boundary is hit — deep cover, third man, or a change of bowler — and the coaching staff upstairs decides which one to invoke based on the match situation.

The debugger you see when an unhandled error occurs in a Lisp REPL is not a crash screen — it is a live handler-bind at the top of the stack that lists every restart currently available, letting you fix data and resume the computation interactively rather than restarting the whole program.

handler-case always unwinds the stack before its clause body runs, so restart-case options established inside the protected form are gone by the time the handler executes — you cannot call invoke-restart from a handler-case clause the way you can from handler-bind.

  • Common Lisp's condition system separates signaling a problem from deciding how to handle it, unlike try/catch which conflates the two.
  • define-condition creates CLOS-based condition classes, typically subclassing error, warning, or condition.
  • error signals and enters the debugger if unhandled; warn signals and simply continues after printing a message.
  • handler-case unwinds the stack before running its clause, offering simple, safe, exception-like recovery.
  • handler-bind runs in the original dynamic environment without unwinding, which is required to invoke restarts.
  • restart-case publishes named recovery strategies at the point of failure without deciding which one to use.
  • invoke-restart, use-value, and store-value let a handler (or a human in the debugger) choose how execution resumes.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#LISPStudyNotes#ErrorHandlingAndConditions#Error#Handling#Conditions#Condition#ErrorHandling#StudyNotes#SkillVeris