Skip to content

Unhandled exceptions in asyncio sometimes pass silently. #97827

Closed as not planned
@trin5tensa

Description

@trin5tensa

According to the docs,

"When an exception is not handled at all, the interpreter terminates execution of the program, or returns to its
interactive main loop. In either case, it prints a stack traceback, except when the exception is SystemExit."

I'm guessing that 'interactive main loop' was intended to cover the peculiar way tkinter works. If that is so, any peculiar behaviour of asyncio's main loop is not explicitly defined.

❌ 1 - Threads + Asyncio

In the minimal and reproducible code example below the unhandled exception does not produce a stack trace unless it is explicitly intercepted with a bare except in the try-except handler. It silently returns to the asyncio loop without any indication that an unhandled error has occurred. (This thread + asyncio design is a pattern for interworking tkinter with asyncio and is recommended in Modern Tkinter by Mark Roseman).

The exception is raised in a coroutine function initiated from a different thread using asyncio. run_coroutine_threadsafe(func(*args), loop).

If the exception is intercepted with a bare except statement the stack trace begins at aio_blocker_controller and not at the asyncio main loop.

✅ 2 & 3 - Pure asyncio & pure threading.

This problem is not seen in a pure threaded or a pure asyncio example.

For asyncio code the equivalent call of await func(*args) produces a correct stack trace beginning at asyncio.run(main()).

For threaded code the equivalent call of threading.Thread(target=func, *args)) also produces a correct stack
trace beginning at Thread.run.

Code Example 1 - Threads + Asyncio

import asyncio
import sys
import threading
import time
import traceback
from typing import Callable, Optional

aio_loop: Optional[asyncio.AbstractEventLoop] = None


async def aio_blocker(block: float = 0.0):
    await asyncio.sleep(block)
    raise ValueError


async def aio_blocker_controller(func: Callable):
    try:
        await func()
        
    # # Handle unhandled exceptions!?
    # except:
    #     traceback.print_exception(*(sys.exc_info()))
    
    finally:
        print('aio_blocker_controller completed normally.')


async def manage_aio_loop(aio_initiate_shutdown: threading.Event):
    global aio_loop
    aio_loop = asyncio.get_running_loop()
    while not aio_initiate_shutdown.is_set():
        await asyncio.sleep(0)


def aio_main(aio_initiate_shutdown: threading.Event):
    asyncio.run(manage_aio_loop(aio_initiate_shutdown))


def main():
    aio_initiate_shutdown = threading.Event()
    aio_thread = threading.Thread(target=aio_main, args=(aio_initiate_shutdown,))
    aio_thread.start()
    
    while not aio_loop:
        time.sleep(0)
    asyncio.run_coroutine_threadsafe(aio_blocker_controller(aio_blocker), aio_loop)
    time.sleep(0.2)
    
    aio_initiate_shutdown.set()
    aio_thread.join()
    

if __name__ == '__main__':
    sys.exit(main())
 

Code Example 2 - Pure Asyncio

import asyncio
from typing import Callable


async def aio_blocker(block: float = 0.0):
    await asyncio.sleep(block)
    raise ValueError


async def aio_blocker_controller(func: Callable):
    try:
        await func()
    finally:
        print('aio_blocker_controller completed normally.')


async def main():
    await aio_blocker_controller(aio_blocker)
    
    
if __name__ == '__main__':
    asyncio.run(main())
 

Code Example 3 - Pure Threading

import sys
import threading
import time
from typing import Callable


def blocker(block: float = 0.0):
    time.sleep(block)
    raise ValueError


def blocker_controller(func: Callable):
    try:
        func()
    finally:
        print('blocker_controller completed normally.')


def main():
    thread = threading.Thread(target=blocker_controller, args=(blocker, ))
    thread.start()
    

if __name__ == '__main__':
    sys.exit(main())
 

Your environment

Python v3.10.7 on MacOS Monterey 12.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    pendingThe issue will be closed if no feedback is providedtopic-asyncio

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions