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)gather failed entirely: server returned 500Here 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) ok: Oslo: +4
error: server returned 500
ok: Tromsø: +4return_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")no answer within 0.3 sEvery 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
| Rule | Why |
|---|---|
Independent requests — through gather | three times faster on three requests (article 2.10) |
gather — with return_exceptions=True | one error must not lose the rest |
| Every network call — with a timeout | otherwise the handler hangs forever |
What to tell the AI
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.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:
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 withreturn_exceptions=True.Without it, one error loses every successful result.
When unpacking
gatherresults, checkisinstance(r, Exception).Every network call gets a timeout, or the handler hangs forever.
The test question: "what if one server never answers?"
requestsandtime.sleepinside anasync defblock everything.