A hagyományos imperatív programozási nyelvekhez képest a Lisp tágabb lehetőségeket nyújt a kivételek kezelésére. Hagyományosan egy kivétel kezelésének két szereplője van: valaki a callstack tetején eldobja a kivételt, és egy másik kódrészlet, a callstackben lejjebb, elkapja azt. A módszer nyilvánvaló hátránya, hogy az elkapásnak elég közel kell lennie a kiváltás helyéhez ahhoz, hogy értelmesen kezelni tudjuk, ugyanakkor kellően magas szinten is kell állnia ahhoz, hogy jó döntést tudjon hozni a kezelés mikéntjéről.
Lispben ezzel szemben három szereplős a történet:
Például képzeljünk el egy programkönyvtárat, amellyel valamilyen adott formátumban lévő file-okat olvasunk be. Előfordulhat olyan eset, hogy a file nem a formátumának megfelelő: ekkor valahol a file-feldolgozóban keletkezik kivétel.
Ekkor feljebb, a file-beolvasás legfelső szintjén a helyzetnek egy nyilvánvaló kezelési módja, hogy helyette egy másik file-t nyitunk meg. Ehhez azonban valahonnan kell kerítenünk egy újabb file-nevet. Mármost lehet, hogy ezt a file-kezelő kódrészletet sok modulból hívjuk, és minden modulnak más lehet a stratégiája arra vonatkozóan, hogy mik az alternatív file-elérési utak. Egyes modulok pedig szeretnének ilyenkor inkább hibajelzéssel leállni, például mert a hiányzó file nem helyettesíthető. Ezért aztán innen egy olyan újrakezdési lehetőséget küldünk tovább, hogy egy file-név ismeretében megpróbálkozhatunk egy másik file beolvasásával.
Nyilvánvalóan a beolvasót használó modulok belsejében találjuk meg a döntéshozó kódot: az egyik hívó például ilyen esetben úgy dönt, hogy a FILE.IN file olvasási hibája esetén próbálkozzunk a FILE.IN.BAK file-lal, a másik pedig nem próbálkozik újrakezdésekkel, inkább kiír egy hibaüzenetet a felhasználónak.
Lássuk, milyen nyelvi elemekkel támogatja a Lisp az ilyen szerkezetű alkalmazások létrehozását!
Kivételeket a DEFINE-CONDITION makróval definiálhatunk. Egy kivétel valójában egy speciális osztály; ennek megfelelően a DEFINE-CONDITION szintaxisa is hasonló a DEFCLASS makróéhoz:
Ha nem adunk meg ősosztályt, akkor a CONDITION lesz az, ez az általános nemlokális visszatérések ősosztályát jelenti. Ha kifejezetten hibajelenség jelzésére használjuk a kivételt, akkor az ERROR osztályból származtassunk.
A kivétel kiváltására a legáltalánosabb lehetőség a SIGNAL függvény. Ez a függvény NIL értekkel tér vissza, ha senki sem kapta el a kivételt, vagyis a futás folytatódik az eldobás helyét követően (vagyis ez nem igazán felel meg a hagyományos throw fogalomnak). Ehhez hasonló a WARN függvény is, amely alapesetben, vagyis ha nem kezelték le a kivételt, a kimenetre naplózza a hibát és folytatja a végrehajtást.
A hagyományos kivétel-dobásnak a ERROR függvény felel meg Lispben: ha nem kezeli senki a kivételt, akkor a futtatókörnyezettől függően vagy a debuggerbe jutunk, vagy egyszerűen leáll az alkalmazás.
A fentiek alapján már el tudjuk kezdeni a bevezetőben vázolt file-feldolgozó alkalmazás vázát:
A kivételkezelők a program egy részét úgy hajtják végre, hogy ha azokban meghatározott típusú kivételek lépnek fel, akkor a helyzetet különböző újrakezdésekkel tudják kezelni. Ehhez a RESTART-CASE makrót használjuk:
Ez a makró megpróbálja kiszámítani kifejezés értékét, és ha közben nem történik kivétel, akkor ezzel az értékkel tér vissza. Ha viszont kivétel történik, akkor felajánlja a különböző megoldásokat, és ha ezek közül egy döntéshozó kód valamelyiket kiválasztja, akkor az annak megfelelő kifejezés értékével tér vissza.
Az eddigi példánkat tovább folytatva megváltoztathatjuk a PROCESS-FILE függvény definícióját: ha hiba lép fel a sorok feldolgozása során, akkor vagy üres listával térünk vissza, vagy megpróbálkozhatunk egy másik file-lal.
A felkínált újrakezdéseket a INVOKE-RESTART függvénnyel hívhatjuk meg. Az egyes kivétel-típusokhoz pedig a HANDLER-BIND makróval rendelhetünk kezelő-függvényeket.
A HANDLER-BIND makróban használt kezelőknek olyan formátumban kell lenniük, hogy paraméterként megkaphassák a tényleges kivétel-példányt. A legegyszerűbb erre a célra névtelen függvényeket használnunk, mint azt az alábbi példában is láthatjuk, amely a file-beolvasó kirakósjátékunk utolsó darabja:
Látható, hogy az első függvény hiba esetén üres listával tér vissza, a második pedig, ha létezik biztonsági másolat, akkor megpróbálja azt beolvasni, egyébként pedig nem kezeli a hibát (vagyis hagyja leállni az alkalmazást).
Természetesen a fent leírt rendszert nem kötelező teljes egészében kihasználnunk Lispben sem: a HANDLER-CASE makró segítségével megvalósíthatjuk a hagyományos, kétszereplős kivételkezelést is. Ennek szintaxisa:
Ennek a viselkedése megegyezik a hagyományos imperatív nyelvekben látott kivételkezeléssel: a HANDLER-BIND kiszámítja a kifejezés értékét, és ha közben nem történik kivétel, akkor visszaadja ezt az értéket; ha viszont kivétel történik, akkor a kiváltott kivétel típusa alapján megkeresi a megfelelő kezelőt, és azt futtatja le (a megfelelő változó értéke lesz a konkrét kivétel-példány). Például az alábbi kifejezés visszatérési értéke 42 lesz: