ExceptionGroup and except* — Multiple Exceptions at Once
~18 min · exceptiongroup, except-star, 3.11, asyncio
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
The new shape (3.11+) — multiple exceptions in flight
Until Python 3.11, raising and catching exceptions was strictly serial: one exception at a time. But concurrent code — asyncio with TaskGroup, parallel I/O, batch operations — sometimes has multiple things fail simultaneously. ExceptionGroup is the language's answer: a single exception that wraps a list of others, and except* is the new syntax for handling specific types within the group.
ExceptionGroup — wraps a list of exceptions
You raise ExceptionGroup(message, list_of_excs). The group itself is an exception; the constituent exceptions are accessible via .exceptions. Tracebacks show the whole tree.
except* — catch a type, leave the rest
except* SomeError as eg: catches all SomeErrors within the group, regardless of nesting depth. The bound eg is a new ExceptionGroup containing only the matched ones. Other exceptions in the original group continue to propagate; multiple except* blocks can each catch their own type.
asyncio.TaskGroup — where you'll meet this first
The most common producer of ExceptionGroup in real code is asyncio.TaskGroup (3.11+). If multiple tasks fail concurrently, the group bundles all their exceptions into one ExceptionGroup. Handling becomes try: async with TaskGroup() as tg: ...; except* OneType as eg: ...; except* OtherType as eg: ....
Principle: Most application code doesn't need to construct ExceptionGroups directly. You'll mainly encounter them when using TaskGroup or other concurrent primitives. Knowing the except* syntax means you can handle multi-failure cases cleanly when they arise.
Code
Raising and catching an ExceptionGroup·python
# Python 3.11+
try:
raise ExceptionGroup(
"multiple problems",
[
ValueError("bad value"),
KeyError("missing"),
TypeError("wrong shape"),
],
)
except* ValueError as eg:
print("caught value errors:", [str(e) for e in eg.exceptions])
except* KeyError as eg:
print("caught key errors:", [str(e) for e in eg.exceptions])
# TypeError in the original group propagates further up — neither except* matched it
ExceptionGroup with asyncio.TaskGroup (3.11+)·python
# import asyncio
#
# async def fail_a():
# raise ValueError("a failed")
#
# async def fail_b():
# raise KeyError("b failed")
#
# async def main():
# try:
# async with asyncio.TaskGroup() as tg:
# tg.create_task(fail_a())
# tg.create_task(fail_b())
# except* ValueError as eg:
# print("value errors:", [str(e) for e in eg.exceptions])
# except* KeyError as eg:
# print("key errors:", [str(e) for e in eg.exceptions])
#
# asyncio.run(main())
# Both tasks ran concurrently; both failures collected into the group; both handled.
Nested ExceptionGroups — except* sees them flat·python
try:
raise ExceptionGroup(
"outer",
[
ValueError("v1"),
ExceptionGroup("inner", [ValueError("v2"), KeyError("k1")]),
],
)
except* ValueError as eg:
print("all ValueErrors:", [str(e) for e in eg.exceptions])
# Both v1 (top level) and v2 (nested) — except* recurses
except* KeyError as eg:
print("all KeyErrors:", [str(e) for e in eg.exceptions])
Without except* — single-pick handling (legacy style)·python
# If you don't have 3.11 or want the old-style handling:
try:
raise ExceptionGroup("trouble", [ValueError("x"), KeyError("y")])
except ExceptionGroup as eg:
for e in eg.exceptions:
print("individual:", type(e).__name__, e)
# This works but doesn't handle nesting — for that you need except*
Construct an ExceptionGroup with three constituent exceptions: a ValueError, a KeyError, and another nested ExceptionGroup containing a TypeError and a second ValueError. Then write a try/except* block that handles all ValueErrors in one branch and all KeyErrors in another. Print the exception messages from each branch. Verify that nested ValueErrors are correctly grouped together by except*.
Progress
Progress is local-only — sign in to sync across devices.