Programming with AI
ENRU
6.4

Asynchrony in practice

timeouts, parallel requests and how it breaks

What it is

Article 2.10 explained what async and await are. Here are the three things real code needs and the AI most often gets wrong: running things in parallel, handling errors while doing so, and timeouts.

The topic is practical: almost all the asynchronous code in a bot or a service consists of those three things.

What it looks like in code

import asyncio


async def fetch(city: str, delay: float) -> str:
    await asyncio.sleep(delay)
    if city == "Bergen":
        raise RuntimeError("server returned 500")
    return f"{city}: +4"

Running in parallel, and its trap

try:
    await asyncio.gather(fetch("Oslo", 0.1), fetch("Bergen", 0.1))
except RuntimeError as e:
    print("gather failed entirely:", e)
measured
gather failed entirely: server returned 500

Here is what matters: one failure killed the whole set. The request to Oslo succeeded, and its result is lost — gather raised the first exception and threw the rest away.

For a bot polling ten cities, that means: one unreachable server and the user gets nothing at all.

How it should be

results = await asyncio.gather(
    fetch("Oslo", 0.1), fetch("Bergen", 0.1), fetch("Tromsø", 0.1),
    return_exceptions=True,
)
for r in results:
    print("  error:" if isinstance(r, Exception) else "  ok:", r)
measured
  ok: Oslo: +4
  error: server returned 500
  ok: Tromsø: +4

return_exceptions=True turns exceptions into ordinary items in the list. Now the successful results are kept and the failures can be handled individually.

The price is the isinstance(r, Exception) check when unpacking. Without it an exception travels on as an ordinary value and surfaces somewhere unexpected.

Timeouts

A request that never returns will stop the handler forever:

try:
    await asyncio.wait_for(fetch("Oslo", 2.0), timeout=0.3)
except TimeoutError:
    print("no answer within 0.3 s")
measured
no answer within 0.3 s

Every network call must have a timeout. Libraries usually provide their own (aiohttp.ClientTimeout), but if you don't set it explicitly, the default may be minutes.

Three rules

RuleWhy
Independent requests — through gatherthree times faster on three requests (article 2.10)
gather — with return_exceptions=Trueone error must not lose the rest
Every network call — with a timeoutotherwise the handler hangs forever

What to tell the AI

Prompt
These three requests are independent. Do them with asyncio.gather
and return_exceptions=True, then unpack the results: collect the
successful ones into a list, log the failures with the city name.
Prompt
Add a 5-second timeout to every network call.
On a timeout, log a warning and skip that city rather than
failing the whole command.

Checking finished code:

Prompt
What happens in this code if one of the three servers
doesn't answer? And if it never answers at all?

The second question catches a missing timeout — something invisible both at runtime and in tests.

What to look for

gather without return_exceptions. The main sign. Visible at a glance.

No timeout. Look for ClientTimeout, timeout=, wait_for. If none of them is there, there is no timeout.

A loop with await instead of gather. The requests run one after another when they could run together. Not an error, but three times slower (article 2.10).

A synchronous library inside an async def. requests, time.sleep, a plain open — they block everything. The asynchronous replacements: aiohttp, httpx, aiosqlite, aiofiles.

asyncio.run inside an already running loop. The error RuntimeError: asyncio.run() cannot be called from a running event loop means code was copied from a script example into a bot handler.

A task created and never awaited. asyncio.create_task(...) with no await anywhere later: the task may not finish before the program does.

Cheat sheet

  • Independent requests through gather, always with return_exceptions=True.

  • Without it, one error loses every successful result.

  • When unpacking gather results, check isinstance(r, Exception).

  • Every network call gets a timeout, or the handler hangs forever.

  • The test question: "what if one server never answers?"

  • requests and time.sleep inside an async def block everything.