Open In App

Error Handling in LISP

The condition system in Lisp is one of its best features. It accomplishes the same task as the exception handling mechanisms in Java, Python, and C++ but is more adaptable. In reality, its adaptability goes beyond error handling since conditions, which are more flexible than exceptions, can represent any event that occurs while a program is running and might be of interest to code at various levels on the call stack. The condition system is more flexible than exception systems because it divides the duties into three parts: signaling a condition, handling it, and restarting. Exception systems divide the duties into two parts: code that handles the issue and code that signals the condition..

A condition is an object whose class describes its general characteristics and whose instance data contains information on the specifics of the events that led to the condition being indicated. With the exception of the fact that the default superclass of classes defined with DEFINE-CONDITION is CONDITION rather than STANDARD-OBJECT, condition classes are defined using the DEFINE-CONDITION macro.



To define a condition:

Syntax:

(define-condition condition-name (error)
  ((text :initarg :text :reader text))
)



Catching any Condition (Handler-case):

A code used to handle the situation signaled there on is known as a condition handler. The erroring function is typically called from one of the higher level functions where it is typically defined. The signaling mechanism looks for a suitable handler when a condition is notified based on the condition’s class. Each handler has a type specifier that indicates the situations it may handle as well as a function that only accepts the condition as its only argument.

The signaling mechanism locates the most recent handler that is compatible with the condition type and calls its function whenever a condition is signaled. A condition handler is created by the macro Handler-case.

Syntax:

(handler-case (code that errors out)
  (condition-type (the-condition)       

 ;; <– optional argument
     (code))                                          

;;<– code of error clause
  ( another-condition (the-condition)
    …))

The Handler-case returns its value if the code that encounters an error returns normally. A Handler-body case’s must consist of a single expression; PROGN can be used to aggregate many expressions into a single form. The code in the relevant error clause is executed, and its value is returned by the Handler-case, if the code that fails to execute signals a condition that is an instance of any of the condition-types listed in any error-clause.

Catching a Specific Condition:

By writing the condition-type, we may tell condition handlers what condition to handle. This process is comparable to a try/catch found in other programming languages, but we can accomplish more.

Example 1:




;; LISP code for error handling
(handler-case (/ 3 0)
(division-by-zero (c)
(format t "Caught division by zero: ~a~%" c)))

Output:

 

You will frequently encounter the following compiler warning if you keep the condition object as an argument but don’t access it in your handlers:

; caught STYLE-WARNING:
; The variable C is defined but never used.

Use a declare call as in the following to remove it:

(handler-case (/ 3 0)
 (division-by-zero (c)
 (declare (ignore c))

(format t “Caught division by zero~%”)))

;; we don’t print “c” here and don’t get the warning.

Handling conditions (Handler-bind):

Condition handlers can respond to a condition by activating the appropriate restart from the Restarting Phase, which is the code that really recovers your application from problems. The restart code is typically inserted into low-level or middle-level functions, while the condition handlers are positioned in the application’s higher levels. By using the Handler-bind macro, you can continue at lower level functions without having to unwind the function call stack and give a restart function. In other words, the lower level function will continue to have control over the process. The Handler-bind syntax is as follows:

(handler-bind ((a-condition #’function-to-handle-it)         

;;<– binding
 (another-one #’another-function))
 (code that can…)
 (…error out))

Each binding has a handler function with one argument and a condition type. The invoke-restart macro searches for and calls the most recent restart function that has been bound, passing the supplied name as an argument.

Example 2:




;Lisp program to demonstrate defining 
; a condition and then handling it using handler-bind
  
;If the divisor argument is zero, 
; the program will produce an error situation.
;there are three anonymous functions(cases) 
; offer three different strategies to overcome it.
; ( return 0, recalculate using divisor 3 
; and continue without returning)
  
  
(define-condition dividing-by-zero (error)
   ((message :initarg :message :reader message))
)
     
(defun div-zero-handle ()
   (restart-case
      (let ((result 0))
         (setf result (div-func 24 0))
         (format t "The value returned is: ~a~%" result)
      )
      (continue () nil)
   )
)
       
(defun div-func (val1 val2)
   (restart-case
      (if (/= val2 0)
         (/ val1 val2)
         (error 'dividing-by-zero :message "denominator is zero")
      )
  
      (return-zero () 0)
      (return-val (x) x)
      (recalculate-with (s) (div-func val1 s))
   )
)
  
(defun high-level-code ()
   (handler-bind
      (
         (dividing-by-zero
            #'(lambda (i)
               (format t "Error is: ~a~%" (message i))
               (invoke-restart 'return-zero)
            )
         )
         (div-zero-handle)
      )
   )
)
  
(handler-bind
   (
      (dividing-by-zero
         #'(lambda (i)
            (format t "Error is: ~a~%" (message i))
            (invoke-restart 'return-val 0)
         )
      )
   )
   (div-zero-handle)
)
  
(handler-bind
   (
      (dividing-by-zero
         #'(lambda (i)
            (format t "Error is: ~a~%" (message i))
            (invoke-restart 'recalculate-with 3)
         )
      )
   )
   (div-zero-handle)
)
  
(handler-bind
   (
      (dividing-by-zero
         #'(lambda (i)
            (format t "Error is: ~a~%" (message i))
            (invoke-restart 'continue)
         )
      )
   )
   (div-zero-handle)
)
  
(format t "Finished executing all cases!"))

Output:

 

Handler-case VS Handler-bind:

The try/catch forms used in other languages are comparable to handler-case. When we need complete control over what happens when a signal is raised, we should utilize a handler-bind. It restarts either interactively or programmatically and lets us use the debugger.

The fact that the handler function bound by Handler-bind will be invoked without unwinding the stack, keeping control in the call to parse-log-entry when this function is called, is a more significant distinction between Handler-bind and Handler-case. The most recent bound restart with the specified name will be found and invoked by the invoke-restart command. We can observe restarts (created by restart-case) wherever deep in the stack, including restarts established by other libraries whose functions this library called, if some library doesn’t catch all situations and allows some bubble out to us. And we can see the stack trace, which includes every frame that was called, as well as local variables and other things in some lisps. Everything is unwound once we forget about this after handling a case. The stack is not rewound by handler-bind.

Signaling (throwing) Conditions:

In addition to the “Condition System,” A number of functions are also available in common LISP that can be used to signal errors. However, how an error is handled once it has been reported depends on the implementation.

A message of error is specified by the user program. The functions analyze this message, and they might or might not show it to the user. Since the LISP system will handle them according to its preferred style, the error messages do not need to contain a newline character at either the beginning or end or to signal error. Instead, they should be created using the format function.

The following are a few often used routines for warnings, breaks, and both fatal and non-fatal errors:

error format-string &rest args

We can use error in two ways:

cerror continue-format-string error-format-string &rest args

warn format-string &rest args

break &optional format-string &rest args

Example 3:




; Lisp program to show signaling of 
; condition using error function.
; Program calculates square root of a number 
; and signals error if the number is negative.
  
(defun Square-root (i)
   (cond ((or (not (typep i 'integer)) (minusp i))
      (error "~S is a negative number!" i))
      ((zerop i) 1)
      (t  (sqrt i))
   )
)
  
(write(Square-root 25))
(terpri)
(write(Square-root -3))

Output:

 

Custom Error Messages:

So far, whenever an error is thrown or signaled, we saw a default text in the debugger which displayed the condition-type. This default error message in the debugger can be customized according to the wish of programmer with the help of :report function.

Default message in debugger:

Condition COMMON-LISP-USER::MY-DIVISION-BY-ZERO was signaled.
 [Condition of type MY-DIVISION-BY-ZERO]

we can write the :report function in the condition declaration to specify the message that will be displayed in debugger when the condition is thrown.

Example:

(define-condition my-division-by-zero (error)
 ((dividend :initarg :dividend
            :initform nil
            :accessor dividend))

 ;; the :report is the message into the debugger:
 (:report (lambda (condition stream)
    (format stream “You were going to divide ~a by zero.~&” (dividend condition)))))

Message in debugger:

 You were going to divide 3 by zero.
   [Condition of type MY-DIVISION-BY-ZERO]

Conditions Hierarchy:

The hierarchy of different conditions is depicted below:

The following is the simple-error class precedence list:

The following is the simple-warning class precedence list:


Article Tags :