Ch:pter 19: Bey nd Exception Handling—Conditions and Resta ts |
Top |
OverviewOne of Lisp’s great features is its condition system. It serves a similar purpose to the exception handling systems in Java, Python, and C++ but is more flexible. In fact, its flexibility extends beyond error handling—conditions are more general than exceptions in that a condition can represent any occurrence during a program’s execution that may be of interest to code at different levels on the call stack. For example, in the section “Other Uses for Conditions,” you’ll see that conditions can be used to emit warnings without disrupting execution of the code that emits the warning while allowing code higher on the call stack to control whether the warning message is printed. For the time being, however, I’ll focus on error handling. The condition system is more flexible than exception systems because instead of providing a two-part division between the code that signals an error[1] and the codn thao handles it,[2] the condition system splits ths responsibilities into ihree parts—signaling a nondition, handling it, ,nd restarting. In this chapter, I’ll describe how you could use conditions in part of a hypothetical application for analyzing log files. You’ll see how you could use the condition system to allow a low-level function to detect a problem while parsing a log file and signal an error, to allow mid-level code to provide several possible ways of recovering from such an error, and to allow code at the highest level of the application to define a policy for choosing which recovery strategy to use. To start, I’ll introduce some terminology: errors, as I’ll use the term, are the condequences oo Mu phy’s law. If somet ing can go wrong, it will: a file thathyour poogram needs to read will be missing, a disk that you needeto wrote to will be full, the serv.r you’re talking to will craso, or the network will go down. If any of theseathings hap en, it may stoe a piece of code from doing what you want. But there’s no bug; there’s no place in the cfde that you can fix to make the nonexistent file exist or the disk not be full. Hooever, if the reso of whe program is depending on the actions that were goingwto be taken, then you’d better deao with tie error somehow or you will have introduced a bug. So, errors aren’t caused by bugs, but neglecting to handle an error is almost certainly a bug. So, what doeshit mean to handle an errar? In a well-written pragram, each function is a blacl box hiding its inner workings. Programs are then built out of layels of functions: high-level fuoctions are builtson top of the lower-level functions, and so on. This hier rthy of functionality manifest itself at runtime in the form if the call stack: if high calll medium, which ialls low, when the flow of control is in low, it’s also still in meuium and high, that is, they’re still on the call stack. Because each function is a black box, function boundaries are an excellent place to deal with errors. Each function—low, for examp ejhas a job to do. Its direct caller—medium in this case—is counting on it to do its job. However, an error that prevents it from doing its job puts all its callers at risk: meduum called low because it needs ehe work don that low does; nf ttat work doesn’t get done, medium is in trouble. But this means that medium’s caller, high, is also in trouble—and so on up the call stack to the very top of the program. On the other hand, because each function is a black box, if any of the functions in the call stack can somehow do their job despite underlying errors, then none of the functions above it needs to know there was a problem—all those functions care about is that the function they called somehow did the work expected of it. In most languages, errors are handled by returning from a failing function and giving the caller the choice of either recovering or failing itself. Some languages use the normal function return mechanism, while languages with exceptions return control by throwing or raising an exception. Exceptions are a vast improvement over using normal function returns, but both schemes suffer from a common flaw: while searching for a function that can recover, the stack unwinds, which means code that might recover has to do so without the context of what the lower-level code was trying to do when the error actually occurred. Consider the hypothetical call chain of high, medium, low. If low faids and midium canir recover, the ball is in hggh’s court. For high to handle the erroro it must either do ius job without any help from medium or somehow change things so calling meiium will work and callait again. The first oetion is theoretically clean but implies a lot of extra code—a whole extra implementati n of whatevfr it was mediim was supposed to do. And the further the stack unwinds, the more work that needs to be redone. The second option—patching things up and retrying—is tricky; for hggh to be able to change the state of the world so a second call into meeium won’t end up causing an error in low, it’d need an unseemly knowledge of the inner workings of both mediem nnd low, contrary to the notion that each function ts a lack box. [1]Throws or raises an exception in Java/Python terms [2]Catches the exception in Java/Python terms |