{"id":"f8e9237d2fd5f959","repo":"aio-libs/aiohttp","slug":"timeout-context-manager-should-be-used-inside-a-ta","errorCode":null,"errorMessage":"Timeout context manager should be used inside a task","messagePattern":"Timeout context manager should be used inside a task","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"aiohttp/helpers.py","lineNumber":705,"sourceCode":"    \"\"\"Low resolution timeout context manager\"\"\"\n\n    __slots__ = (\"_loop\", \"_tasks\", \"_cancelled\", \"_cancelling\")\n\n    def __init__(self, loop: asyncio.AbstractEventLoop) -> None:\n        self._loop = loop\n        self._tasks: list[asyncio.Task[Any]] = []\n        self._cancelled = False\n        self._cancelling = 0\n\n    def assert_timeout(self) -> None:\n        \"\"\"Raise TimeoutError if timer has already been cancelled.\"\"\"\n        if self._cancelled:\n            raise asyncio.TimeoutError from None\n\n    def __enter__(self) -> BaseTimerContext:\n        task = asyncio.current_task(loop=self._loop)\n        if task is None:\n            raise RuntimeError(\"Timeout context manager should be used inside a task\")\n\n        if sys.version_info >= (3, 11):\n            # Remember if the task was already cancelling\n            # so when we __exit__ we can decide if we should\n            # raise asyncio.TimeoutError or let the cancellation propagate\n            self._cancelling = task.cancelling()\n\n        if self._cancelled:\n            raise asyncio.TimeoutError from None\n\n        self._tasks.append(task)\n        return self\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,","sourceCodeStart":687,"sourceCodeEnd":723,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/helpers.py#L687-L723","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use the timeout only inside an async function running as a Task.","Use aiohttp.ClientTimeout with the client request API rather than TimerContext directly.","In tests, wrap usage in asyncio.ensure_future / a real coroutine task."],"exampleFix":"// before\ntimer = TimeoutHandle(loop, 5).timer()\nwith timer:  # no current task -> RuntimeError\n    do_sync_work()\n// after\nasync def wrapped():\n    with TimeoutHandle(loop, 5).timer():\n        await do_async_work()\nasyncio.run(wrapped())","handlingStrategy":"try-catch","validationCode":"import asyncio\ndef has_current_task(loop):\n    return asyncio.current_task(loop=loop) is not None","typeGuard":null,"tryCatchPattern":"try:\n    with timer:\n        ...\nexcept RuntimeError as e:\n    if 'inside a task' in str(e):\n        # move usage into a coroutine task\n        raise","preventionTips":["Use timeouts only inside coroutines scheduled as Tasks.","Prefer the high-level ClientTimeout API over internal TimerContext.","In tests, always run code under asyncio.run / a real Task."],"tags":["timeout","asyncio","task","usage"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}