Debugging Lisp Part 4: Restarts

  • I built cl-async and a lot of its satellite libraries completely ignoring restarts and the CL error handling paradigm in general, opting more for a "well, an error occurred, catch it and send it to a callback" method.

    A few lengthy discussions with some lispers who were more well-versed in CL's error handling than what would be the equivalent of try/catch/throw completely changed my view on lisp's abilities. The reasoning behind using handler-bind instead of handler-case just blew my mind once I understood it. It was a hard concept to grasp, but I'm so glad those guys pounded it in.

    I reprogrammed the error handling in cl-async/blackbird/wookie/etc to use handler-bind/restarts and now everything is so much clearer. You get full stacktraces when in debug mode, but also have the option to pass errors around as objects if in production.

    CL's error handling definitely was a "wow" moment for me once I got it.

  • This is one of my favorite features of Common Lisp programming. The nice thing about interactive restarts is that the whole stack is there... so all of the previous functionality is available as discussed previously in the series. You can interactively inspect an object to see what slot values are causing a conflict and not only change the value and continue but recompile the method that caused it as well. And of course clever code could do this as well.

    Restarts are a very nice way of handling known error conditions and I wish more languages had stolen this feature from Lisp before macros or garbage collectors and things.

  • Restarts are nice.

    The thing is, though, is that restarts are relatively easy to add to any language that has well-supported structures for non-local control flow; they're just a protocol, after all. That's really what Common Lisp has which most other languages lack. Common Lisp (very intelligently, IMHO) did not conflate error handling with non-local control flow; they are different concepts and deserve to be treated independently.

    Specifically, the lexcal scoping of BLOCK and TAGBODY labels and the fact that RETURN-FROM and GO will unwind the stack if necessary to get to the target label make creating something like HANDLER-BIND/RESTART-BIND relatively easy. (There is also of course THROW and CATCH [which don't really have anything to do with error handling in CL, they're just structures for dynamically-scoped stack unwinding], but I've generally found the need to create explicit labels for those less elegant than using lexical scoping. You can always use a closure if you need to store the lexicaly-bound state to be used somewhere else, and the reverse is not as easy...)

    You obviously also need UNWIND-PROTECT to keep things safe when the stack unwinds, but most languages successfully manage to get that with something like try-finally (or RAII in C++'s case, even if I find it clumsy sometimes).

  • This has been a great series: clearly written, solid examples, and obvious utility. Thanks for doing it!