In languages with "exceptions", when an exception gets thrown, you have a chance to catch it, but at that point the stack has already been unwound and there is no way to resume execution at the point where exception was thrown.
In CL, you can let someone else (the caller) make the decision on what to do in case of an exception, providing options. For example, if a value is a NaN, the caller could request to continue the computation using the caller-supplied value instead.
In practical terms, this system, while very impressive, it isn't widely understood or used in CL.
It's one of the very few things I miss after I moved from CL to Clojure. Rich Hickey wrote that he did consider it, but it was a lot of complexity for fairly little gain (all platforms where Clojure is hosted support thrown exceptions only), which I would agree with.
I agree and wholeheartedly hope that my book mends that issue, even if just partially.
The credit goes to the author of that library, though. :)
All the best for your book!
It might not be easy to tell Clojure or the JVM to not be trigger happy with throwing exceptions and therefore destroying the stack. Perhaps one will find a way through!
With traditional exceptions, the stack unwind all the way to the "catch" block which has full responsibility of handling the exception. With CL conditions, you can implement multiple handlers which do not catch the exception, and then a handler way up in the stack can decide which of the low-level handlers to run.
It sounds bonkers, but when you've tried it you'll see that "normal" exceptions are a very crippled version of it.
I'd say that this is the main difference that sets the CL condition system apart from mainstream programming languages. In these other languages, when an error happens, the stack is automatically unwound; in Lisp, when an error happens, the stack is automatically wound further, and the code executed in such way has the full choice of whether to unwind the stack and where exactly to unwind it to; this information is provided in the dynamic environment in which a given piece of code is running.
When you divide by zero in Java, then there is nothing set in stone that prevents the language from winding the stack further and executing some code that will analyze the dynamic environment in which the error happened, calling all error handlers found in the dynamic environment, and only then - if no handler transferred control outside the error side - either entering an interactive debugger of some sorts or giving up and crashing.
But instead Java goes another way and immediately destroys the stack by throwing an exception. That's a design choice made by Java creators. Lisp's homoiconicity has nothing to do with this.
You can also analyze the Python implementation of the condition system posted elsewhere in the comments - that's an example of a condition system in a non-homoiconic language.
The image-based Lisp is just very very different when compared to mainstream batch languages like C, Rust, or Java.
But that only wants to make me learn it properly more.
It also means you don't have to write blinks of try catch nested in while loop just to be able to retry the operation...
However, that doesn't need to be the case; it is still possible to e.g. do the industry standard, which is to log all available information about the request and program state to some sort of log file or crash dumps, return a HTTP 500, and start waiting for another request. The possibility of interactive resolution of exceptional situations is a possibility, not a requirement.
It's also possible to block and invoke the debugger only in some cases, e.g. when some global debug variable is set to true. In all other cases, the system can behave automatically.
>log all available information about the request and program state to some sort of log file
I've only dabbled with some Schemes, but given Lisp's homoiconic nature, I'm guessing it would also be possible to generate and persist a unit test based on the failing call too, right?
As long as you can persist all the state (which is easy if you are programming in a functional manner), then you will know exactly what sort of input caused a crash in your program. And when you have the data, you can write it in a manner that is later readable by the Lisp reaer.
However think of all the things that can go wrong with your environment that don't involve a bug in your program. A network connection goes down, the disk gets full, &c. All of those can be handled without unwinding the stack to the point where the code for handling them was defined.
For a really trivial example, consider a performance watchdog. If you want to completely abort the attempt and log a call-stack, you can do that it just about any modern, dynamic language because the exception object includes the call-stack. But what if you want to log the call-stack and continue for the first N times the watchdog rolls-over? This is, of course, doable in other languages with passing callbacks and such, but in Lisp this can be done by signalling a condition in the exact same manner that an exceptional condition would be signaled.
I'm excited about this book, because that particular feature is underutilized in Lisp (by myself included), probably because it's one of the last dynamic things that Lisp does that more well-known dynamic languages do not, so programmers coming from other languages just aren't going to use it.
Nitpick: the only thing that cannot be handled by winding the stack is when you run out of stack to wind.
But, luckily, if your program overflows the stack, you usually have bigger issues to deal with and/or you usually have all the stack information you need to diagnose the problem.