2010 July 09
Errors or exceptional events can occur during the execution of a program. The common idiom to handle exceptions is to return control to the caller function. This is done either by returning an error code or by throwing an exception object. Languages like C++, Java and Python allow the programmer to create an object that contain information about the error. This exception object is then handed over to the runtime system. This is known as throwing the exception. When an exception is thrown, the runtime system start unwinding the call stack, looking for a function with a special block of code that can handle the exception. This code block is called the exception handler. When an exception handler is found, the exception object is handed over to it. This is known as catching the exception. The code block can now decide what to do with the exception. It can retry the failed operation, log the error state, re-throw the exception or just ignore it. This mechanism has some clear advantages. For instance, languages like Java has checked exceptions that make it impossible for caller methods to ignore an exception event. The exception system also helps is separating error-handling code from regular code. But returning control to the caller has some disadvantages as well. The exception handling code might be high above the stack. This means unwinding the whole stack. This may not be the ideal thing to do, when the exceptional situation can be corrected locally. For example, think of a function that makes a socket connection to a remote server:
// This code is not Java or C#, but
// an illustration.
Socket connect (String hostName, int port)
throws SocketConnectionError {
if (!ResolveHost (hostName))
throw new SocketConnectionError (
"Host cannot be resolved.");
....
return newSocketObject;
}
Now if hostName was input from a UI and the user misspelled it, this function will throw an exception and the whole sequence of function calls has to be started again, while the error can be corrected by directly replacing the value of hostName without leaving the current stack. The exception system used by most languages do not offer this flexibility. Now let us turn our attention to a superior way of dealing with errors: the condition system of Common Lisp.
Before plunging into the full depths of the condition system, let us see how Common Lisp allows the handling of exceptions in the traditional way. In Common Lisp exception objects are called Conditions. A new Condition is defined using the define-condition macro. Conditions are sub-types of the error class. This is how a condition to signal socket errors could be defined:
(define-condition socket-connection-error (error)
((text :initarg :text :reader text)))
The constructor of socket-connection-error will take an argument called text that describes the error. Now we can use socket-connection-error in the Common Lisp version of the SocketConnect function:
(defun socket-connect (host port)
(if (not (resolve-host host))
(error 'socket-connection-error
:text "Failed to resolve host."))
....
new-socket-object)
The macro handler-case can be used to implement the Java style exception handling mechanism by unwinding the stack:
(handler-case
(socket-connect "www.lisp.org" 80)
(socket-connection-error (ex)
(format t "error: ~a~%" (text ex))))
This is similar to a try-catch block. If socket-connect fails, the socket-connection-error object is caught and the text description of the error is printed to the output stream.
Another form used to catch exceptions is restart-case. This is similar to handler-case except that it does not specify any exceptions to be caught. Instead it contain named code blocks that will be executed in case of an error. These code blocks are called restarts.
(defun get-page (host port page)
(restart-case
(let ((sock (socket-connect host port)))
(socket-http-get sock page))
(get-default-content ()
"<html><body>Page not found</body></html>")))
The above code handles a socket-connection-error with the help of a restart named get-default-content. It returns a pre-defined HTML encoded message in case of a socket connection error. If this is executed from a Common Lisp shell, the user will land in a debugger, where he will be given a list of restarts that include get-default-content. To evade the debugger and execute a preferred restart directly, we can use the handler-bind macro:
(handler-bind ((socket-connection-error
#'(lambda (ex)
(invoke-restart
'get-default-content))))
(get-page "www.lisp.org" 80 "/"))
handler-bind take two forms as arguments. The first form contain exception handler functions. The above code explicitly invokes the get-default-content restart. To the end user, this looks as if the get-page call has succeeded and returned valid HTML. Restarts are what makes the Common Lisp condition system special. With the help of restarts exceptions can be handled without unwinding the stack. Restarts also allow functions to expose a list of error handling policies. This give callers the opportunity to choose one error handling strategy over another. Let us study this in detail.
Error handling policies are implemented by providing more than one restart. Here is a new version of get-page that expose three policies: one that returns a default HTML document, another that will return the value passed by the caller and yet another that simply prints the exception message. A caller can choose any one that suits his need:
(defun get-page (host port page)
(restart-case
(let ((sock (socket-connect host port)))
(socket-http-get sock page))
(get-default-content ()
"<html><body>Page not found</body></html>")
(use-value (v) v)
(print-error (ex)
(format t "Error: ~a~%" (text ex)))))
Someone that opts to pass in a user defined HTML document on a socket connection error can call get-page like this:
(handler-bind ((socket-connection-error
#'(lambda (ex)
(invoke-restart 'use-value
"<html><body>Network error!</body></html>"))))
(get-page "www.lisp.org" 80 "/"))
It is even possible to make the restart read a value from the end user, so that exception handling becomes more interactive:
;; more restarts
(defun get-page (host port page)
(restart-case
(let ((sock (socket-connect host port)))
(socket-http-get sock page))
(get-default-content () "hello, world")
(use-value (v)
:report "Specify HTML to return"
:interactive (lambda () (list (read)))
v)
(print-error (ex) (format t "Error: ~a~%" (text ex)))))
The debugger will provide the user with the prompt "Specify HTML to return" where he can specify the value to be returned by get-page. (Asking the user to type in HTML will not be a pleasant UI experience, though!)
The following updated version of get-page makes the interactive restart feature more practical. It gives the user an option to specify a new URL to connect to. A connection is attempted until success. The exception is fixed just were it occurred, without losing context:
(defun get-page (host port page)
(loop
(restart-case
(let ((sock (socket-connect host port)))
(return (socket-http-get sock page)))
(use-value (v) (setf host v)))))
A user level function can now retry the connection from the get-page stack itself:
(defun download-page (url page)
(handler-bind
((socket-connection-error
#'(lambda (ex)
(invoke-restart 'use-value
(progn
(format t "Connection failed. Enter a new URL:")
(list (read)))))))
(get-page url 80 page)))
A sample interaction with this function is given below. During the first try the user misspelled the URL. But he will get a chance to correct the typo, without unwinding the stack and calling download-page again!
> (download-page "www.lips.og" "doc.html")
Connection failed. Enter a new URL:
=> www.lisp.org
"<html><body><h1>The Common Lisp HyperSpec</h1>......"
The scope of the condition system is not limited to error handling. We can build signalling protocols on top of this so that functions can interact with each other in complex ways. The error function we used to raise a condition is built on top of a primitive called signal. This can be called directly to use conditions as a communication mechanism between functions. The following example shows how signal can be used to interleave execution of two functions:
(define-condition hand-over-control (error) nil)
(defun f ()
(format t "f1 ")
(signal (make-condition 'hand-over-control))
(format t "f2 "))
(defun g ()
(handler-bind
((hand-over-control #'(lambda (hand-over-control-object)
(format t "g1 "))))
(f))
(format t "g2~%"))
A call to g will produce the following output:
> (g)
f1 g1 f2 g2
References: