Python asyncio.Task异常未触发直至主任务完成的技术问询
Let's break down why your code behaves this way, and point you to the asyncio code that handles these unawaited task exceptions.
The Core Issue: Unawaited Tasks Store Exceptions Instead of Crashing the Loop
When you create an asyncio.Task (like you did with asyncio.Task(coro1())) but never await it or retrieve its result, asyncio doesn't immediately propagate the exception to the event loop. Instead, it:
- Stores the exception in the Task object's
_exceptionattribute - Marks the Task as finished
- Waits until either:
- You explicitly call
task.result()ortask.exception()(which will re-raise the exception) - The Task is garbage collected (at which point asyncio logs a warning about the unhandled exception)
- The event loop exits entirely
- You explicitly call
In your code, coro2 is the main task that run_until_complete is waiting on. The asyncio.sleep(10) keeps this task alive for 10 seconds, so the event loop keeps running even though the coro1 tasks have already thrown exceptions. Only when coro2 finishes does the loop exit.
Where Asyncio Handles These Uncaught Task Exceptions
Let's look at the key parts of the asyncio source code that manage this behavior:
1. Task _step Method: Capturing the Exception
When a Task executes and hits an exception, the _step method (in asyncio.tasks.Task) catches it and calls _set_exception:
# Simplified snippet from asyncio.tasks def _step(self, exc=None): try: if exc is None: result = self._coro.send(None) else: result = self._coro.throw(exc) except StopIteration as exc: self._set_result(exc.value) except Exception as exc: self._set_exception(exc) # This stores the exception ...
2. _set_exception Stops the Task Without Propagating
The _set_exception method marks the Task as finished but doesn't trigger any loop termination:
def _set_exception(self, exception): if self._state != _PENDING: return self._exception = exception self._state = _FINISHED self._schedule_callbacks() # Runs any attached callbacks, but not loop-wide error handling
3. Garbage Collection Logs Unhandled Exceptions
If the Task is never awaited or queried for its result, its __del__ method will eventually trigger a warning:
def __del__(self): if not self._done: ... elif self._exception is not None and not self._log_destroy_pending: self._log_destroy_pending = True logger.warning( "Task exception was never retrieved", exc_info=(type(self._exception), self._exception, self._exception.__traceback__) )
Why Your run_until_complete Breakpoint Didn't Trigger
The run_until_complete method only handles exceptions from the specific future it's waiting on (in your case, the coro2 task). The exceptions from the coro1 tasks are stored in their own Task objects, not propagated to the main future. That's why the except block in run_until_complete never fires.
How to Fix This (If You Want Immediate Crashes)
If you want unhandled task exceptions to crash the loop immediately, you have a few options:
- Await all tasks you create (e.g., use
asyncio.gather(*ts)instead of just creating them) - Attach a callback to each task that re-raises the exception into the loop:
def handle_task_exception(task): try: task.result() except Exception as e: loop.call_exception_handler({"exception": e, "task": task}) for task in ts: task.add_done_callback(handle_task_exception) - Use
asyncio.create_task(Python 3.7+) and enableloop.set_exception_handlerto catch unhandled exceptions.
内容的提问来源于stack exchange,提问作者YNX

