asyncio TimeoutError and CancelledError (tested)

asyncio.wait_for and asyncio.timeout raise TimeoutError, but the code being timed sees CancelledError, and getting that wrong hides timeouts entirely. We measured each case on Python 3.13.5.

1. What wait_for raises, and why the message is empty

import asyncio
async def slow():
    await asyncio.sleep(5)
async def main():
    try:
        await asyncio.wait_for(slow(), timeout=0.1)
    except TimeoutError as e:
        print(type(e).__name__, repr(str(e)))
    print(asyncio.TimeoutError is TimeoutError)
asyncio.run(main())

Output:

TimeoutError ''
True

Measured on Python 3.13.5: asyncio.wait_for raises the built-in TimeoutError, and asyncio.TimeoutError is TimeoutError is True (they were merged in 3.11, per the docs; on 3.10 and earlier they are different classes, so catch asyncio.TimeoutError there). The message is an empty string, so log.error(f"failed: {e}") logs failed: with nothing after it. Log repr(e) or the exception type.

2. asyncio.timeout() works by cancelling

import asyncio
async def main():
    try:
        async with asyncio.timeout(0.1):
            await asyncio.sleep(5)
    except TimeoutError:
        print("timed out")
    # the trap: catching CancelledError inside the timed block
    try:
        async with asyncio.timeout(0.1):
            try:
                await asyncio.sleep(5)
            except asyncio.CancelledError:
                print("inner saw CancelledError (that's how timeouts work)")
                raise
    except TimeoutError:
        print("outer saw TimeoutError")
asyncio.run(main())

Output:

timed out
inner saw CancelledError (that's how timeouts work)
outer saw TimeoutError

Measured: async with asyncio.timeout(0.1) (3.11+) raises TimeoutError outside the block, but the code inside sees asyncio.CancelledError first. That's the mechanism: the timeout cancels the task, then converts the cancellation into TimeoutError at the boundary. If you catch CancelledError inside to clean up, re-raise it.

3. Swallowing CancelledError silently defeats the timeout

import asyncio
async def main():
    # swallowing the CancelledError defeats the timeout
    async def stubborn():
        try:
            await asyncio.sleep(5)
        except asyncio.CancelledError:
            return "swallowed"
    loop = asyncio.get_running_loop(); t0 = loop.time()
    try:
        r = await asyncio.wait_for(stubborn(), timeout=0.1)
        print("no TimeoutError, got", repr(r), f"after {loop.time()-t0:.1f}s")
    except TimeoutError:
        print("TimeoutError")
asyncio.run(main())

Output:

no TimeoutError, got 'swallowed' after 0.1s

Measured: a coroutine that catches CancelledError and returns makes wait_for return that value with no TimeoutError at all, so the caller thinks the operation succeeded. Never swallow CancelledError; do cleanup in finally or re-raise.

4. except Exception catches TimeoutError but not CancelledError

import asyncio
async def main():
    try:
        await asyncio.wait_for(asyncio.sleep(5), timeout=0.1)
    except Exception as e:        # TimeoutError is an Exception ...
        print("except Exception caught", type(e).__name__)
    t = asyncio.create_task(asyncio.sleep(5))
    await asyncio.sleep(0); t.cancel()
    try:
        await t
    except Exception:
        print("except Exception caught CancelledError")
    except BaseException as e:    # ... but CancelledError is BaseException since 3.8
        print("only BaseException caught", type(e).__name__)
asyncio.run(main())

Output:

except Exception caught TimeoutError
only BaseException caught CancelledError

Measured: TimeoutError is an Exception, but CancelledError derives from BaseException, so a generic except Exception handler lets cancellation through (which is what you want). Catching BaseException or using a bare except: around awaits causes the swallowing bug from section 3.

Frequently asked questions

Why is my asyncio TimeoutError message empty?

wait_for and asyncio.timeout raise TimeoutError with no message; we measured str(e) == '' on Python 3.13.5. Log repr(e) or type(e).__name__.

Is asyncio.TimeoutError the same as TimeoutError?

Yes on Python 3.11+: we measured asyncio.TimeoutError is TimeoutError returning True on 3.13.5. On 3.10 and earlier they are separate classes (not tested here).

Why does my coroutine get CancelledError instead of TimeoutError?

Timeouts work by cancelling the task; the code inside sees CancelledError, and it becomes TimeoutError at the wait_for / asyncio.timeout boundary. We measured both sides.

Why doesn't asyncio.wait_for time out?

If the coroutine catches CancelledError and returns, wait_for returns that value with no error. We measured exactly that after 0.1 s. Re-raise CancelledError.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 on Linux: asyncio.wait_for and asyncio.timeout raising TimeoutError, its empty message, CancelledError inside the timed block, a coroutine that swallows cancellation, and which except clauses catch each. Every block was run verbatim and its output pasted from that run. Python 3.10 and earlier were not tested. PlainHeaders is our extension; everything else here is the standard library.