Asyncio terminating all tasks when one of them throws exception
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When several asyncio tasks are running together, you often want fail-fast behavior: if one task crashes, cancel the rest and stop the whole operation. The right solution depends on your Python version.
In modern Python, asyncio.TaskGroup is the cleanest answer because it automatically cancels sibling tasks when one fails. In older code, you usually need to create tasks manually, catch the exception, cancel the remaining tasks, and wait for the cancellations to finish.
The Modern Solution: TaskGroup
If you are on Python 3.11 or later, use asyncio.TaskGroup. It was designed for structured concurrency.
If task b fails, the task group cancels the other running tasks and then re-raises the failure as part of an exception group.
This is the preferred pattern because cancellation and cleanup are handled in one place.
How to Do It Before TaskGroup
Older asyncio code often used asyncio.gather, but gather alone does not give you the cleanest fail-fast control unless you add explicit cancellation logic.
A robust manual pattern looks like this:
The second gather(..., return_exceptions=True) is important. It gives cancelled tasks a chance to finish their cancellation path cleanly instead of leaving them half-finished.
Why Plain gather Can Be Misleading
Developers often assume await asyncio.gather(*tasks) automatically means “if one fails, stop everything.” The behavior is more nuanced.
If one task raises an exception, gather propagates that exception to the caller. But other tasks may still need explicit cancellation handling, especially if you created them separately and want to control shutdown behavior predictably.
That is why structured concurrency with TaskGroup is such a useful improvement.
Make Your Tasks Cancellation-Friendly
Cancellation only works well if tasks cooperate. A task should not swallow asyncio.CancelledError unless it re-raises it after cleanup.
If tasks block in non-awaiting code or suppress cancellation incorrectly, “cancel all tasks” becomes much less reliable.
When return_exceptions=True Is Not the Right Tool
asyncio.gather(..., return_exceptions=True) is sometimes useful when you want all tasks to finish and then inspect every result or failure. That is not the same as fail-fast cancellation.
If your goal is “shut everything down when one task fails,” use TaskGroup or explicit cancellation, not a blanket “turn exceptions into results” approach.
Common Pitfalls
A common mistake is cancelling sibling tasks but never awaiting them afterward. That can leave warnings about unfinished tasks or hide cleanup errors.
Another mistake is catching CancelledError and not re-raising it. That breaks normal cancellation semantics.
Developers also overuse return_exceptions=True when they actually want fail-fast behavior. It changes the control flow and can hide failure signals.
Finally, do not assume every background task should be cancelled on first failure. Some systems want graceful degradation instead. Choose fail-fast behavior intentionally.
Summary
- On Python 3.11 and later, use
asyncio.TaskGroupfor fail-fast task management. - In older code, create tasks manually, catch the exception, cancel the others, and await their cancellation.
- Always make long-running tasks cancellation-friendly.
- '
return_exceptions=Trueis for result collection, not for clean fail-fast shutdown.' - The key idea is not just cancelling tasks, but cancelling them and waiting for shutdown to complete properly.

