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
- t
The following is the simple-warning class precedence list:
- simple-warning
- simple-condition
- warning
- condition
- t
Similar Reads
Error Handling in Perl Error Handling in Perl is the process of taking appropriate action against a program that causes difficulty in execution because of some error in the code or the compiler. Processes are prone to errors. For example, if opening a file that does not exist raises an error, or accessing a variable that
5 min read
File Handling in LISP LISP is an acronym for list processing, it is one the oldest programming language currently being used in the field of artificial intelligence. It performs its computations on symbolic expressions which makes it too reliable for AI. File handling is a method by which we can access files and store da
4 min read
Exception handling in Julia Any unexpected condition that occurs during the normal program execution is called an Exception. Exception Handling in Julia is the mechanism to overcome this situation by chalking out an alternative path to continue normal program execution. If exceptions are left unhandled, the program terminates
6 min read
Arrays in LISP LISP, is a list processing, is a programming language widely used in working with data manipulation. LISP allows us to produce single or multiple-dimension arrays using the make-array function. An array can store any LISP object as its elements. Â The index number of the array starts from 0 to the n-
2 min read
Dolist Construct in LISP DoList in Common LISP is a looping statement used to iterate the elements in a list. Syntax: (dolist input_list) statements... ) Here, The input_list contains the list of elements that are iterated.The statements are present in the loop. Example 1: LISP Program to iterate the list of elements from 1
2 min read
How to Handle length Error in R Length errors in R typically occur when attempting operations on objects of unequal lengths. For example, adding two vectors of different lengths will result in an error. These errors can also be seen when operations are performed on arrays or other data structure. Hence, it is crucial to understand
4 min read
If Construct in LISP In this article, we will discuss the if construct in LISP. The if is a decision-making statement used to check whether the condition is right or wrong. The if the condition is right, then it will go inside the if block and execute the statements under if block. Otherwise, the statements are not exec
2 min read
Do Construct in LISP In this article, we will discuss the Do Construct in LISP. Do construct is used to perform an iteration in a structured format. Syntax: (do ((variable1 value1 new updated value 1) (variable2 value2 new updated value 2) ------------------------------- ------------------------------- (variable n value
2 min read
How to Implement Error Handling in R Programming Error handling is a crucial aspect of programming that allows to identify, and gracefully manage errors or exceptions that may occur during the execution of code. In R Programming Language, effective error handling can enhance the reliability of scripts and applications. What are Errors in R?An erro
6 min read
Functions in LISP A function is a set of statements that takes some input, performs some tasks, and produces the result. Through functions, we can split up a huge task into many smaller functions. They also help in avoiding the repetition of code as we can call the same function for different inputs. Defining Functio
3 min read