This is a good article about the benefits of conditions and restarts, but I've always been a bit suspicious of adding special-purpose mechanisms for this. I think you can do something similar with normal functions (closures) and datatypes, but first some motivation:
One thing that exceptions don't interact with very well is concurrency. For example, the Alice ML programming language[1] had a nice approach to concurrency using futures where you could just insert a "spawn" keyword in front of any expression to make it run in a background thread, returning a (transparent) future for the eventual result. This was pretty neat, but it fell apart somewhat around the use of exceptions. That is, given code like the following:
let x = some_computation()
handle DivByZero() => ...
you can't just simply put a "spawn" in front of some_computation() because now any exception raised will happen in the background thread, which doesn't have the exception handler on the stack. (At least, that's how I remember Alice ML working, and that's how it works in most languages with exceptions).
My feeling at the time was that rather than exception handling walking up the stack to find a handler, the try/catch (or handle) statement should instead create a closure at the point it is executed that is then passed down as part of the dynamic environment to subsequent calls. The raise/throw statement would then find a matching closure from the dynamic environment and immediately call it at the point the exception is raised. This makes exception/condition handlers into normal functions, and also means that this dynamic environment can be copied to background threads when they are spawned.
In this approach, restarts can be handled via normal datatypes. That is, the code that was raising an error would do something like this (pseudocode):
type restart = Skip | Retry | Abort
if malformed(record):
let restart = signal MalformedRecord(record)
match (restart):
on Skip -> /* do nothing */
on Retry -> parse(record)
on Abort -> return
end
end
and callers would then do something like:
try parse(stuff)
on MalformedRecord(record) -> Skip
end
The language I was designing at the time was agent-based and a message sent to an agent would start a new transaction. There was a special "fail" statement that could be used to completely abort the current transaction and signal a failure back to the caller, providing an escape hatch when you really did want to unwind the whole stack (and cancel any spawned background tasks).
Anyway, this was fun to think about again. There's so much more design space to explore around error handling IMO.
Edit: I should add that this is pretty much how explicit promises work in most languages that use them, where error handlers are attached to a promise object that is passed down to asynchronous tasks. So in a sense my sketch here is just another form of syntactic sugar over that existing pattern.
[1]: https://en.wikipedia.org/wiki/Alice_(programming_language)