Resumable Exceptions? (original) (raw)

November 13, 2024, 4:20pm 1

(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!"

AlSweigart (Al Sweigart) November 13, 2024, 5:42pm 2

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

zhangyx (Yuxuan Zhang) November 13, 2024, 5:45pm 3

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)

Nineteendo (Nice Zombies) November 13, 2024, 6:00pm 4

Would exceptions be resumable by default? That seems troublesome:

def foo():
    raise NotImplementedError

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

zhangyx (Yuxuan Zhang) November 13, 2024, 6:09pm 5

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

pf_moore (Paul Moore) November 13, 2024, 6:11pm 6

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.

zhangyx (Yuxuan Zhang) November 13, 2024, 6:12pm 7

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).

bschubert (Brian Schubert) November 13, 2024, 6:25pm 8

pf_moore (Paul Moore) November 13, 2024, 6:27pm 9

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.

gcewing (Greg Ewing) November 13, 2024, 11:34pm 10

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.

ImogenBits (Imogen) November 14, 2024, 12:35am 11

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?

Rosuav (Chris Angelico) November 14, 2024, 12:44am 12

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.

blhsing (Ben Hsing) November 14, 2024, 1:03am 13

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)).