I’ve always felt like error handling in Clojure isn’t quite a solved problem, and I wanted to learn about conditions and restarts since they felt like they fit the Clojure model of decoupling things. I wrote this library so that I could use it myself in my projects and see how it feels. After trying a bunch of conditions libraries that I could find, I wasn’t satisfied with any of them. They usually either had problems with threading, didn’t fully support restarts, didn’t support ClojureScript, or didn’t fully support all the primitives from CL. This library seeks to solve all those problems.
The project readme includes a full introduction to conditions and restarts for programmers who don’t have much experience with them, or don’t know the CL operators used with them.
For myself, I plan on using the library in some projects to see if it solves some of the issues I’ve been having.
The ClojureScript support isn’t quite up to 100% just yet, but I have relatively little experience with ClojureScript and haven’t been able to determine the cause of a couple of bugs just yet. PRs are of course welcome if anyone is able to determine the cause.
This is really great! I’ve quite recently seen your library posted on Reddit and it finally made me to look into the topic of restarts and try to understand the concept with the built in language facility of dynamic scope. Gonna say, your library seemed really clean and approachable, far more than using dynamic vars!
I’ve briefly touched your library at the end of that post, and I’m curious what’s your take on the composition problem, and the fact that most error handling is done via exceptions, and mixing both conditions and exceptions may make code harder to reason about. After all it’s fair to say that condition system in Common Lisp is the de-facto standard way of handling errors, and doing other things, and they don’t have to worry about exceptions as a separate thing. I see that your library can work with exceptions but I don’t think it will free us from using try/catch?
Actually it does! The wrap-exceptions macro acts effectively as a restart-case that also turns any exceptions that try to cross its boundary into conditions! These conditions follow farolero’s normal path, counting as errors and as such handlers in their inheritance hierarchy, and :farolero.core/error will all apply to them. This means you can completely replace try/catch in your codebase with wrap-exceptions and handler-case. That said, I didn’t implement unwind-protect from CL, so you will need try/finally for those cases, even if you only use conditions.
While yes, this doesn’t mean that every exception thrown gets turned into a condition, but regular Clojure has the same problem if an exception is thrown in an unexpected place.
Additionally, in the case that you do need to work with exceptions and there’s no way around it, farolero shouldn’t get in the way, because the unwind mechanism used extends from java.lang.Error, which means Java code catching Exception (and not Throwable) won’t interfere with farolero’s unwinding, and it means you can use try/catch without fear of breaking your restarts.
One thing I would like some feedback on however: the current default system debugger is like CLs debugger. I stand by this, but the debugger hook is bound to nil by default, meaning the system debugger is invoked.
I would like this library to be usable in library code as well, not only applications. The way to allow this to be done is to ensure that conditions don’t introduce any new concepts when signaled without handlers bound. The only change this requires to farolero as it stands is to make there be a default debugger hook which will throw the condition as an exception.
So my question is, given a default behavior of throwing the conditions as exceptions as the default debugger (with the option to bind the existing debugger if you want it), would you consider using farolero in library code, as well as applications?
I’ve released version 1.0.2 as a bugfix release which changes the default behavior to throwing an exception when an unhandled condition is raised. If the condition raised is already an exception and no additional arguments are passed to it, then it will throw the condition as-is, otherwise it will wrap it in an ex-info. The readme has been updated to explain this behavior, and how you can recover the interactive debugger for applications or development.
This was a bugfix release because I consider it a bug that I hadn’t thought about this before I released it as a full release.
Nice! Have you considered making all exceptions into conditions when used in any of condition handling primitives?
With the last change that automatically throws unhandled conditions as exceptions, it seems like a good opportunity to also make reverse situation automatic, e.g. allowing handling exceptions as conditions automatically.
This would make the library more transparent. On the other hand this may be too magical?
As a user, you can do this by using wrap-exceptions around calls that may throw. The reason I can’t do this as a part of the library is that there’s no way I can distinguish exceptions that are thrown by the debugger (which are allowed to be of any type) or other cases where you do want to throw exceptions, from when you want to transparently convert exceptions into conditions. If I made all the macros convert exceptions to conditions transparently, then debuggers can’t throw exceptions, and I can’t make this library’s behavior transparent. If I were taking that tack, then I would be taking the opposite perspective I’m currently taking, that is to prefer conditions over default exception handling, and it’d make the most sense to ensure an interactive debugger was invoked by default instead of one that throws exceptions since that wouldn’t work in this context.