Skip to content

Commit 66d717e

Browse files
committed
PEP 830: Rework Motivation example to actually produce multiple errors
The original TaskGroup example only ever produced one sub-exception because TaskGroup cancels siblings on first failure. Switch to a three-backend asyncio.gather(..., return_exceptions=True) pattern that collects every failure, and replace the illustrative output with real output captured from the reference implementation. Update the sys.excepthook rejection to drop the now-stale TaskGroup reference.
1 parent fc3de33 commit 66d717e

1 file changed

Lines changed: 37 additions & 36 deletions

File tree

peps/pep-0830.rst

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ Currently there is no standard way to obtain this information. Python authors
3131
must manually add timing to exception messages or rely on logging frameworks,
3232
which can be costly and is inconsistently done and error-prone.
3333

34-
Consider an async service that fetches data from multiple backends concurrently
35-
using ``asyncio.TaskGroup``. When several backends fail, the resulting
36-
``ExceptionGroup`` contains all the errors but no indication of their temporal
37-
ordering::
34+
Consider an async service that fetches data from multiple backends
35+
concurrently and reports every failure rather than failing fast. The
36+
resulting ``ExceptionGroup`` contains all the errors in submission order,
37+
with no indication of when each one occurred::
3838

3939
import asyncio
4040

@@ -50,52 +50,53 @@ ordering::
5050
await asyncio.sleep(2.3)
5151
raise TimeoutError("Recommendation service timeout")
5252

53-
async def fetch_inventory(items):
54-
await asyncio.sleep(0.8)
55-
raise KeyError("Item 'widget-42' not found in inventory")
56-
5753
async def get_dashboard(uid):
58-
async with asyncio.TaskGroup() as tg:
59-
tg.create_task(fetch_user(uid))
60-
tg.create_task(fetch_orders(uid))
61-
tg.create_task(fetch_recommendations(uid))
62-
tg.create_task(fetch_inventory(['widget-42']))
54+
results = await asyncio.gather(
55+
fetch_user(uid),
56+
fetch_orders(uid),
57+
fetch_recommendations(uid),
58+
return_exceptions=True,
59+
)
60+
errors = [r for r in results if isinstance(r, Exception)]
61+
if errors:
62+
raise ExceptionGroup("dashboard fetch failed", errors)
6363

6464
asyncio.run(get_dashboard("usr_12@34"))
6565

6666
With ``PYTHON_TRACEBACK_TIMESTAMPS=iso``, the output becomes:
6767

6868
.. code-block:: text
6969
70-
Traceback (most recent call last):
71-
...
72-
ExceptionGroup: unhandled errors in a TaskGroup (4 sub-exceptions)
70+
+ Exception Group Traceback (most recent call last):
71+
| File "service.py", line 26, in <module>
72+
| asyncio.run(get_dashboard("usr_12@34"))
73+
| ...
74+
| File "service.py", line 24, in get_dashboard
75+
| raise ExceptionGroup("dashboard fetch failed", errors)
76+
| ExceptionGroup: dashboard fetch failed (3 sub-exceptions) <@2026-04-19T07:24:31.102431Z>
7377
+-+---------------- 1 ----------------
7478
| Traceback (most recent call last):
75-
| File "service.py", line 11, in fetch_orders
76-
| raise ValueError(f"Invalid user_id format: {uid}")
77-
| ValueError: Invalid user_id format: usr_12@34 <@2025-03-15T10:23:41.142857Z>
79+
| File "service.py", line 5, in fetch_user
80+
| raise ConnectionError(f"User service timeout for {uid}")
81+
| ConnectionError: User service timeout for usr_12@34 <@2026-04-19T07:24:29.300461Z>
7882
+---------------- 2 ----------------
7983
| Traceback (most recent call last):
80-
| File "service.py", line 7, in fetch_user
81-
| raise ConnectionError(f"User service timeout for {uid}")
82-
| ConnectionError: User service timeout for usr_12@34 <@2025-03-15T10:23:41.542901Z>
84+
| File "service.py", line 9, in fetch_orders
85+
| raise ValueError(f"Invalid user_id format: {uid}")
86+
| ValueError: Invalid user_id format: usr_12@34 <@2026-04-19T07:24:28.899918Z>
8387
+---------------- 3 ----------------
8488
| Traceback (most recent call last):
85-
| File "service.py", line 19, in fetch_inventory
86-
| raise KeyError("Item 'widget-42' not found in inventory")
87-
| KeyError: "Item 'widget-42' not found in inventory" <@2025-03-15T10:23:41.842856Z>
88-
+---------------- 4 ----------------
89-
| Traceback (most recent call last):
90-
| File "service.py", line 15, in fetch_recommendations
89+
| File "service.py", line 13, in fetch_recommendations
9190
| raise TimeoutError("Recommendation service timeout")
92-
| TimeoutError: Recommendation service timeout <@2025-03-15T10:23:43.342912Z>
91+
| TimeoutError: Recommendation service timeout <@2026-04-19T07:24:31.102394Z>
92+
+------------------------------------
9393
94-
The timestamps immediately reveal that the order validation failed first
95-
(at .142s), while the recommendation service was the slowest at 2.3 seconds.
96-
That could also be correlated with metrics dashboards, load balancer logs, or
97-
traces from other services or even logs from the program itself to build a
98-
complete picture.
94+
The sub-exceptions are listed in submission order, but the timestamps reveal
95+
that the order validation actually failed first (at 28.899s), the user
96+
service half a second later, and the recommendation service last after 2.3
97+
seconds. These can also be correlated with metrics dashboards, load
98+
balancer logs, traces from other services, or the program's own logs to
99+
build a complete picture.
99100

100101

101102
Specification
@@ -401,7 +402,7 @@ Using ``sys.excepthook``
401402

402403
``sys.excepthook`` runs only when an uncaught exception reaches the top
403404
level, at display time rather than when the exception was created. For the
404-
motivating ``TaskGroup`` example, the hook would fire once for the resulting
405+
motivating example above, the hook would fire once for the resulting
405406
``ExceptionGroup`` after all tasks have completed, so every sub-exception
406407
would receive the same timestamp. Exceptions that are caught and logged
407408
never reach the hook at all.
@@ -622,7 +623,7 @@ Change History
622623
coarse-resolution clock; reworded the Runtime API rejection.
623624
- Added an Open Issues section covering display location, benchmarking the
624625
``sys.monitoring`` alternative, and recording time of first raise.
625-
- Made the Motivation example self-contained.
626+
- Reworked the Motivation example to be self-contained.
626627

627628

628629
Copyright

0 commit comments

Comments
 (0)