python/cpython · error · ExceptionGroup

staggered race failed

Error message

staggered race failed

What it means

Raised by the internal staggered race helper (Lib/asyncio/staggered.py) as an ExceptionGroup wrapping all sub-exceptions that the race's completion machinery could not attribute to a normal outcome. This helper powers asyncio.open_connection/start_connection's 'happy eyeballs' behavior of trying multiple addresses with delays, so in practice you see it when concurrent connection-attempt tasks die with unexpected exceptions. It is guarded by __debug__ because an unhandled exception here usually indicates a programming error or a broken loop callback rather than a normal network failure.

Source

Thrown at Lib/asyncio/staggered.py:166

        futures.future_add_to_awaited_by(first_task, parent_task)
        running_tasks.add(first_task)
        first_task.add_done_callback(task_done)
        # first_task has been appended to running_tasks before the event loop starts running it.
        propagate_cancellation_error = None
        # Make sure no tasks are left running if we leave this function
        while running_tasks:
            on_completed_fut = loop.create_future()
            try:
                await on_completed_fut
            except exceptions_mod.CancelledError as ex:
                propagate_cancellation_error = ex
                for task in running_tasks:
                    task.cancel(*ex.args)
            on_completed_fut = None
        if __debug__ and unhandled_exceptions:
            # If run_one_coro raises an unhandled exception, it's probably a
            # programming error, and I want to see it.
            raise ExceptionGroup("staggered race failed", unhandled_exceptions)
        if propagate_cancellation_error is not None:
            raise propagate_cancellation_error
        return winner_result, winner_index, exceptions
    finally:
        del exceptions, propagate_cancellation_error, unhandled_exceptions, parent_task

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Read the wrapped exceptions: catch ExceptionGroup and inspect .exceptions to find the real cause before assuming a network problem
  2. If one address family consistently fails (e.g. broken IPv6), restrict attempts by passing a single resolved address or filtering getaddrinfo results
  3. Reproduce without python -O so the debug guard reports the group deterministically
  4. If the traceback points into asyncio internals, check your Python version against known asyncio bug fixes and upgrade

Example fix

// before
reader, writer = await asyncio.open_connection(host, port)

// after
try:
    reader, writer = await asyncio.open_connection(host, port)
except BaseExceptionGroup as eg:
    for sub in eg.exceptions:
        print('attempt failed:', repr(sub))
    raise
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

try:
    reader, writer = await asyncio.open_connection(host, port)
except* OSError as eg:      # per-attempt network failures
    log.debug('connect attempts failed: %r', eg.exceptions)
    raise
except BaseExceptionGroup as eg:  # staggered race programming errors
    for sub in eg.exceptions:
        log.exception('race task failed', exc_info=sub)
    raise

Prevention

When it happens

Trigger: Calling asyncio.open_connection()/start_connection() (or loop.start_connection()) with multiple resolved addresses where one of the spawned attempt tasks raises an exception not captured by the race's normal exception-recording path; any bug in a custom event loop, transport, or patched asyncio internals that makes the race's on_completed callback itself raise.

Common situations: DNS resolves a host to several addresses (IPv4+IPv6) and one attempt path raises an unexpected error; running under python -O changes whether the guard fires; monkey-patching of asyncio or a custom loop implementation interferes with task done-callbacks; rare upstream asyncio bugs during cancellation storms.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/04af6eef24ebdc22. Report an issue: GitHub.