LISP - Error Handling
Lisp - Modern Error Handling
Section titled “Lisp - Modern Error Handling”Error handling is a critical part of writing robust software. Common Lisp features a uniquely powerful and flexible Condition System. Unlike traditional exception handling in many languages, which simply unwinds the call stack to a handler, the Lisp Condition System allows for a separation between signaling an exceptional situation and deciding how to handle it. This enables the creation of highly resilient and interactive programs.
The system is built on three core concepts:
- Signaling a Condition: Notifying the system that an exceptional event has occurred. This can be a fatal error, a warning, or just a notification.
- Handling a Condition: Code that catches a signaled condition and decides what to do.
- Restarting: A unique feature where the code that signaled the condition can provide recovery strategies. The handler can then choose one of these strategies to resume execution without fully unwinding the stack.
Simple Error Handling: handler-case
Section titled “Simple Error Handling: handler-case”For many situations, handler-case provides a simple and familiar try-catch style of error handling. It executes a block of code and, if a condition of a specified type is signaled, it aborts the block and runs the corresponding handler code.
Example: Robust Division
Section titled “Example: Robust Division”Let’s create a function that safely divides two numbers, gracefully handling the built-in division-by-zero condition.
;;; main.lisp
(defun safe-divide (numerator denominator) "Divides two numbers, returning the result or a descriptive message on error." (handler-case ;; Code to try (/ numerator denominator)
;; Error clause: handles conditions of type 'division-by-zero' (division-by-zero (c) (format nil "Error: Cannot divide by zero. (Details: ~a)" c))))
;; --- Test Cases ---(format t "10 / 2 = ~a~%" (safe-divide 10 2))(format t "10 / 0 = ~a~%" (safe-divide 10 0))(format t "-5 / 2 = ~a~%" (safe-divide -5 2))Output
Section titled “Output”10 / 2 = 510 / 0 = Error: Cannot divide by zero. (Details: Division by zero.)-5 / 2 = -5/2Advanced Interactive Handling: Restarts
Section titled “Advanced Interactive Handling: Restarts”The true power of the Condition System lies in restarts. Restarts allow a low-level function that signals an error to offer recovery options. A high-level handler can then choose one of these options to resume execution, often without the low-level function ever knowing it was paused.
Let’s build a more sophisticated division function that demonstrates this.
Step 1: Define a Custom Condition
Section titled “Step 1: Define a Custom Condition”First, we define our own specific condition type using define-condition. This makes our handlers more precise.
;; Define a specific condition for our application(define-condition invalid-divisor (error) ((value :initarg :value :reader invalid-divisor-value)) (:report (lambda (condition stream) (format stream "The value ~s is an invalid divisor." (invalid-divisor-value condition)))))Step 2: Signal the Condition and Provide Restarts
Section titled “Step 2: Signal the Condition and Provide Restarts”Our division function will signal our custom condition if the denominator is zero. Crucially, it uses restart-case to offer two recovery strategies: one to use a different value, and another to just return NIL.
;; Low-level function that signals the error and offers solutions(defun recoverable-divide (numerator denominator) (if (zerop denominator) ;; If the denominator is zero, signal our condition. (restart-case (error 'invalid-divisor :value denominator) ;; Restart 1: Allow the caller to provide a new denominator. (use-new-denominator (new-denominator) :report "Specify a new denominator to use for the division." (recoverable-divide numerator new-denominator))
;; Restart 2: Allow the caller to simply abort and return NIL. (abort-division () :report "Abort the division and return NIL." nil))
;; If everything is fine, just do the division. (/ numerator denominator)))Step 3: Handle the Condition and Invoke a Restart
Section titled “Step 3: Handle the Condition and Invoke a Restart”Now, a higher-level function can call recoverable-divide and use handler-bind to handle the error. Unlike handler-case, handler-bind does not unwind the stack. It can inspect the condition and then use invoke-restart to choose one of the available recovery options.
;; High-level function that handles the error interactively.(defun division-ui () (handler-bind ((invalid-divisor (lambda (c) (declare (ignore c)) ; we don't need the condition object here (format *error-output* "~&Error signaled! Attempted to divide by zero.~%") (format *error-output* "Let's try to recover by providing a new denominator.~%") ;; Choose the 'use-new-denominator' restart and pass '2' as the new value. (invoke-restart 'use-new-denominator 2)))) (let ((result (recoverable-divide 10 0))) (format t "~&The final result of the division is: ~a~%" result))))
(division-ui)Output
Section titled “Output”Error signaled! Attempted to divide by zero.Let's try to recover by providing a new denominator.The final result of the division is: 5Common Signaling Functions
Section titled “Common Signaling Functions”Here’s a summary of the standard functions used to signal conditions.
| Function | Description and Use Case |
|---|---|
| error datum &rest args | Signals a fatal, non-continuable error. The most common way to signal that something has gone wrong and execution cannot proceed normally without intervention. |
| cerror continue-format-string error-datum &rest args | Signals a continuable error. If this is signaled in an interactive debugger (like SLIME), the user is presented with an option to continue execution. This is extremely useful during development. |
| warn datum &rest args | Signals a non-fatal warning. The program execution continues after the warning is printed. Useful for alerting the user about potential issues, like using a deprecated function. |
| break &optional format-string &rest args | Enters the debugger unconditionally. This is a powerful debugging tool, acting like a breakpoint that also allows interactive inspection and modification of the program state. |
Best Practices and Summary
Section titled “Best Practices and Summary”- Use
handler-casefor simpletry-catchlogic where you just want to handle an error and move on. - Define custom conditions for your application to make handlers more specific and your code more self-documenting.
- Use
restart-caseandhandler-bindfor resilient systems where low-level code can offer recovery strategies and high-level code can decide how to use them. - Leverage
cerrorduring development to create continuable breakpoints when certain conditions are met.