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.
(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.
(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: