aio-libs/aiohttp · error · RuntimeError

Timeout context manager should be used inside a task

Error message

Timeout context manager should be used inside a task

What it means

Raised by TimerContext.__enter__ when asyncio.current_task(loop=self._loop) returns None, i.e. the timeout context manager was entered outside of any asyncio Task. Timeouts must cancel a running task, so entering one with no current task is a usage error (RuntimeError).

Source

Thrown at aiohttp/helpers.py:705

    """Low resolution timeout context manager"""

    __slots__ = ("_loop", "_tasks", "_cancelled", "_cancelling")

    def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
        self._loop = loop
        self._tasks: list[asyncio.Task[Any]] = []
        self._cancelled = False
        self._cancelling = 0

    def assert_timeout(self) -> None:
        """Raise TimeoutError if timer has already been cancelled."""
        if self._cancelled:
            raise asyncio.TimeoutError from None

    def __enter__(self) -> BaseTimerContext:
        task = asyncio.current_task(loop=self._loop)
        if task is None:
            raise RuntimeError("Timeout context manager should be used inside a task")

        if sys.version_info >= (3, 11):
            # Remember if the task was already cancelling
            # so when we __exit__ we can decide if we should
            # raise asyncio.TimeoutError or let the cancellation propagate
            self._cancelling = task.cancelling()

        if self._cancelled:
            raise asyncio.TimeoutError from None

        self._tasks.append(task)
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Use the timeout only inside an async function running as a Task.
  2. Use aiohttp.ClientTimeout with the client request API rather than TimerContext directly.
  3. In tests, wrap usage in asyncio.ensure_future / a real coroutine task.

Example fix

// before
timer = TimeoutHandle(loop, 5).timer()
with timer:  # no current task -> RuntimeError
    do_sync_work()
// after
async def wrapped():
    with TimeoutHandle(loop, 5).timer():
        await do_async_work()
asyncio.run(wrapped())
Defensive patterns

Strategy: try-catch

Validate before calling

import asyncio
def has_current_task(loop):
    return asyncio.current_task(loop=loop) is not None

Try / catch

try:
    with timer:
        ...
except RuntimeError as e:
    if 'inside a task' in str(e):
        # move usage into a coroutine task
        raise

Prevention

When it happens

Trigger: Using aiohttp's internal TimeoutHandle.timer() / async-timeout style context manager from a synchronous callback, in asyncio.run top-level code before a task is created, or from a thread that has no event loop task. Common when a timeout object leaks into non-async code or tests that drive the loop manually.

Common situations: Unit tests calling protocol methods directly without an enclosing task; refactors that move timeout usage out of coroutines; third-party code reusing aiohttp's TimerContext in sync helpers.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/f8e9237d2fd5f959.json. Report an issue: GitHub.