📝Common Lisp: condition system

Condition system in Common Lisp is similar to exceptions in other languages, however it has a couple of differences:

  1. In addition to separating signaling a condition and handling it, it also allows restarts—a function can provide an exception handling/recovery code (a restart) without actually executing it. Functions higher up the stack can select which restart to execute.

  2. It does not unwind the stack automatically. Instead, it select a handler function and executes it, the handler can return value to the signaling function.

Common Lisp’s condition system is more general than exception—it allows implementing arbitrary communication protocols between higher and lower functions.

;; Use `define-condition' to define a new condition type. This
;; function works the same as `defclass', but default superclass is
;; `condition' rather than `standard-object'.
;; `error' is a subclass of condition that usually _has_ to be
;; handled. (If you signal a non-error condition and there is no
;; handlers, the execution continues after call to the `signal'.)
(define-condition malformed-log-entry-error (error)
  ((text :initarg :text
         :reader text)))

;; To create a new condition, use `make-condition' instead of
;; `make-instance'.
(make-condition 'malformed-log-entry-error :text "hello")

;; Or `error' to immediately signal the condition.
(error 'malformed-log-entry-error :text "hello")

;; `handler-case' is the simplest condition-handler that behaves
;; similarly to try-catch in other languages.
;; Basic form:
(handler-case expression
;; where error-clause =
(condition-type ([var]) code)

(handler-case (parse-log-entry text)
  (malformed-log-entry-error () nil))

;; `restart-case' allows defining restarts. The form is similar to
;; `handler-case' but uses arbitrary symbols instead of
;; condition-type.
(restart-case (parse-log-entry text)
  (skip-log-entry () nil))
;; To execute a restart, you need `handler-bind' (`handler-case'
;; doesn't work because it unwinds the stack).
;; The form:
(handler-bind (binding*) form*)

(handler-bind ((malformed-log-entry-error
                 #'(lambda (c)
                     (invoke-restart 'skip-log-entry))))
  (dolist (log (find-all-logs))
    (analyze-log log)))
;; When you create a new restart, it is often helpful to define a
;; “restart function”—a function that calls `invoke-restart' and can
;; be used as a `handler-bind' handler.
(defun skip-log-entry (c)
  (invoke-restart 'skip-log-entry))
;; If there is no `skip-log-entry' restart, this will throw
;; `control-error'.
;; It is possible to test with `find-restart' if the restart is
;; registered before invoking it.
(defun skip-log-entry (c)
  (let ((restart (find-restart 'skip-log-entry)))
    (when restart (invoke-restart restart))))

;; `use-value' is a standard restart name that accepts a value that a
;; function returns instead of error. It also has a restart function
;; `use-value'.

;; In addition to error, there are other signaling functions.
;; - `error': if the condition is not handled, debugger is
;;   invoked. The function does not continue.
;; - `warn': if the condition is not handled, the condition is printed
;;   to `*error-output' and continues execution.
;;   `warn' also establishes a `muffle-warning' restart that makes
;;   `warn' return without printing the condition.
;; - `cerror' similar to `error' in that it invokes a debugger if
;;   condition is not handled.
;;   However, it also registers a `continue' restart that continues
;;   execution after `cerror'.
;; - You can also use `signal' directly or build your own protocols on
;;   top of it.