Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

with exceptiongroup.catch({Exception: async_fn}): is a tempting and silent footgun #66

Closed
Zac-HD opened this issue Jun 29, 2023 · 4 comments · Fixed by #69
Closed

with exceptiongroup.catch({Exception: async_fn}): is a tempting and silent footgun #66

Zac-HD opened this issue Jun 29, 2023 · 4 comments · Fixed by #69

Comments

@Zac-HD
Copy link
Contributor

Zac-HD commented Jun 29, 2023

It's surprisingly easy to write a handler which silently discards exceptions that you thought were handled:

import asyncio
from exceptiongroup import ExceptionGroup, catch

async def handler(eg):
    # Log some stuff, then re-raise
    raise eg

async def main():
    with catch({TypeError: handler}):
        raise ExceptionGroup("message", TypeError("uh-oh"))

asyncio.run(main())

and in a large program, all too easy to miss your only warning:

$ python t.py 
exceptiongroup/_catch.py:52: RuntimeWarning: coroutine 'handler' was never awaited
  handler(matched)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

...the problem being that we expect sync handlers, and so we just call it - which if it's an async function won't actually run the body of the handler, and thus won't re-raise any exception, which is treated as success.


IMO this code is unrecoverably correct, and we should bail out with an error as soon as possible. Based on Trio's internal comments we should rely on checking the return value rather than inspecting the handler callable - I suspect doing an initial branch on is not None would keep things super-fast for the vast majority of happy cases. Thoughts?

@agronholm
Copy link
Owner

We could use inspect.iscoroutinefunction() and raise TypeError or something on a positive. It's an extra step for everyone who's using this correctly, but maybe the performance hit isn't noticeable.

@Zac-HD
Copy link
Contributor Author

Zac-HD commented Jun 29, 2023

I think inspect.iscoroutinefunction() is unreliable for python-trio/trio#2670 - style reasons, and I think checking return values is faster too since we can skip almost all the work in most cases. That way 'exception not raised' is free, 'returns None' is almost free, and remaining cases are suspicious enough that I don't mind doing a check.

I think that reuse of the context manager is rare enough that we should actually prefer the smaller number of checks done when a handler is actually called, to checking every handler when setting up the context. The downside there is that maybe we could have detected it earlier or more reliably in cases where the buggy handler is only very rarely invoked, which could be sufficient to justify having both checks...

@agronholm
Copy link
Owner

Ok, so the consensus is to use inspect.iscoroutine() on the return value?

@Zac-HD
Copy link
Contributor Author

Zac-HD commented Jun 30, 2023

Sounds good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants