Resumable Exceptions?

(MOVED FROM PYTHON-HELP)

I posted under “help” because this is not a fully developed idea. I wish to see how people think about it before elaborating on it.

Reading through exception related posts, a really weird idea popped up in my mind: what if exception control flow can be resumed?


For example:

def invoke(fn: callable):
    if not callable(fn):
        fn = raise TypeError("Give me a callable")
    return fn()

try:
    print(invoke(None))
except TypeError:
    resume lambda: "You got it!"

# prints: "You got it!"

In this example, keyword raise behaves like yield - it has an optional return value. A new keyword resume is used to return to the execution context where the Exception was raised. It optionally takes an argument as the return value of raise.


Another example - resuming from interrupts:

try:
    lock1.acquire()
    # SIGINT here
    flag1 = True
except KeyboardInterrupt:
    flag_term = True
    resume

This is related to an earlier post on atomic def which attempted to provide robustness with the presence of unmasked signals.


Known challenge - not friendly with nested context managers:

# reusing function invoke from 1st example
try:
    with lock:
        print(invoke(None))
except ValueError:
    # bad: lock.__exit__ already executed
    resume lambda: "You got it!"

You might try posting this in the Ideas forum: Ideas - Discussions on Python.org

1 Like

I am a little afraid to post there because I’ve already opened too many posts recently under that topic.

But I think I will do this for one last time. (My apologies to those who feels disrupted)

Would exceptions be resumable by default? That seems troublesome:

def foo():
    raise NotImplementedError

try:
    print(foo()) # None
except NotImplementedError:
    resume

I guess yes, as long as you elect to do so (i.e. resume on NotImplementedError).

I’m pretty sure this has been suggested before, so you should probably hunt out the previous thread as there will have been issues raised in there that you’ll need to address.

Personally, I’ve never had a need for this sort of functionality, so I’m at best neutral on it.

I searched google, stackoverflow, github issues, this forum (and also asked ChatGPT). Did not find anything (I also feel that it should have been suggested, that’s part of reason why I initially posted under help topic).

Here’s a few previous discussions of related ideas, you may be able to find more in the mailing list archives:

1 Like

Hmm, I couldn’t find a discussion either. I’m sure it has been discussed, but if we can’t find the old discussion, that’s fine.

2 Likes

That would be very difficult to implement, considering that exceptions can be propagated through C functions. Once a C function has returned, you can’t resume it.

1 Like

To me it seems like you’d want the function throwing the error to opt into being resumable, otherwise every function that throws errors to deal with invalid state will have really strange behaviour. So if you already have to mark the code in some way, why not just directly use a generator instead?

1 Like

Yeah. Although I’d go a slightly different direction for the same reason: an async function. They’re designed to be easily composable, and you can yield anything you choose to use, so you could design an event loop that permits this sort of resumability.

The important thing here is that exceptions aren’t resumable, but generator and async functions are. There’s a TON of infrastructure to make this happen, which could easily be used for this purpose.

What if the function has two or more raise expressions? How does the caller specify different values for the resume statement to correspond to different raise expressions?

The problem is best solved instead with callback functions (as in codecs.register_error(name, error_handler) with the callback registration approach, or shutil.rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None) with the callback as an argument approach), or simple default values (as in dict.get(key, default)).

3 Likes