Open In App

Error Handling in LISP

Last Updated : 24 Jul, 2022
Improve
Improve
Like Article
Like
Save
Share
Report

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




;; 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




;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

  • A fatal error is indicated by it. Such errors cannot be recovered from, thus they never go back to the person who called them.

We can use error in two ways:

  • (Error “some Text”): Indicates a Simple-Error condition
  • (Error message: “We tried this and that, but it didn’t work.”)

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

  • It enters the debugger and raises an error. However, after fixing the issue, it enables program continuation from the debugger.
  • cerror returns nil if the program is resumed after running into an error. The call to cerror is subsequently followed by the execution of the following code. This code ought to fix the issue, perhaps by asking the user for a new value if a variable was incorrect.
  • The continue-format-string argument is provided as a control string to format along with the args to create a message string, similar to the way the error-format-string argument is.

warn format-string &rest args

  • It typically does not enter the debugger but instead prints an error message.
  • A warn implementation should take care of moving the error message to a new line before and after it, as well as maybe providing the name of the function that called warn.

break &optional format-string &rest args

  • Without any chance of being intercepted by programmed error-handling tools, it outputs the message and enters the debugger right away.
  • Break returns nil if continued. No parameters are required when using break, and a suitable default message will be delivered.
  • It is assumed that using the break command to inject temporary debugging “breakpoints” into a program rather than to indicate problems will prevent any unexpected recovery actions from occurring. Break does not accept the additional format control string argument that cerror accepts, for this reason.

Example 3:

Lisp




; 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:

  • simple-error
  • simple-condition
  • error
  • serious-condition
  • condition

The following is the simple-warning class precedence list:

  • simple-warning
  • simple-condition
  • warning
  • condition
  • t


Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads