# 14.7 Async and await (/building-ai-workers/structuring-programs/async-and-await)

---
type: Document
title: "14.7 Async and await"
description: "When slow calls can run at the same time, which failures are worth one more try, and why every call needs a time limit."
status: stable
order: 314.07
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-10T15:33:14Z
chapter: "14"
part: III
expert_status: builder-only
concepts: [ "14.7" ]
last_verified: 2026-10-30
sources:
  - id: node-globals
    title: "Globals (Node.js, accessed 2026-10-30)"
    resource: https://nodejs.org/api/globals.html
  - id: httpx-async
    title: "Async support (httpx, accessed 2026-10-30)"
    resource: https://www.python-httpx.org/async/
  - id: python-asyncio-tasks
    title: "Coroutines and tasks (Python, accessed 2026-10-30)"
    resource: https://docs.python.org/3/library/asyncio-task.html
generated:
  at: 2026-11-10T15:33:14Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:46fd53c5d7ab1c14f6347908d45c9fdaecda4d914439923ee878bdcecd57803e
dirty: true
ksor_version: 0.0.60
---

**In everyday life.** You have two errands: a suit at the dry cleaner and a prescription at the pharmacy. Each counter needs ten minutes. You can wait at one, then at the other. Or you can hand in both, take a ticket at each, and come back when both are ready. The waits overlap, so the total is the longest wait, not the two added up.

**What you will learn.** When slow calls can run at the same time, which failures are worth another try, and why every call needs a time limit.

**Why it matters.** Most of a program's time goes on waiting. And a call that hangs, which means it waits forever, stops the whole run when nobody is watching.

### Why is most slow work waiting?

Because the slow part is usually somewhere else. The program sits idle while a server answers, a file arrives or a database replies.

- **Async** code lets a program start a slow task, do other work while it waits, and pick the result up when it is ready.
- A **promise** in TypeScript is like the ticket from the counter: it stands for a result that is not ready yet. This is a new meaning of the word, not the promise a signature makes in [13.5](../programming-fundamentals/functions.md). Python's version is a **coroutine**, which differs in one way, as [What if I forget `await`?](#what-if-i-forget-await) explains.
- `await` means "pause this function here until the result is ready". Work already started keeps running meanwhile, but the next line waits.

So two `await`s in a row wait one after the other. To overlap two calls, start both, then wait for both together.

| Idea | TypeScript | Python |
| --- | --- | --- |
| A function that can wait | `async function` | `async def` |
| Wait for one result | `await promise` | `await coroutine` |
| Start several and wait for all | `Promise.all([a, b])` | `asyncio.gather(a, b)` |
| Give up after a time | `AbortSignal.timeout(2000)` on a `fetch`, in milliseconds | `asyncio.timeout(2)`, in seconds |
| Run the program | In a module, Node lets you write `await` outside any function | `asyncio.run(main())` |

### When can two calls run at the same time?

Only when neither needs the other's answer. Vendors and open invoices are independent. Page 1 has to come first, because it says how many pages there are.

**At work.** The [sample API](json-and-rest-apis.md#what-are-the-parts-of-a-request) waits 300 milliseconds per answer, and answers the first vendors call with `503`. One after the other, Ana's client would take about 1.5 seconds: two vendor calls and three invoice pages. With vendors and invoices fetched at the same time, it takes about one second, set by the three pages.

Here are the two errands in code, done both ways. `wait(300, "vendors")` stands in for a slow call: it waits 300 milliseconds, then gives back `"vendors"`. A clock reads the time before and after, in milliseconds. Predict both times.

```ts tab="TypeScript" tab-group="language"
// wait stands in for a slow call. You do not need to follow this line
const wait = (ms: number, label: string) => new Promise<string>((resolve) => setTimeout(() => resolve(label), ms));

// one after the other: wait at the first counter, then at the second
let start = performance.now();
const first = await wait(300, "vendors");
const second = await wait(300, "invoices");
console.log("one after the other", Math.round(performance.now() - start), first, second);

// at the same time: hand in both, then wait for both together
start = performance.now();
const both = await Promise.all([wait(300, "vendors"), wait(300, "invoices")]);
console.log("at the same time", Math.round(performance.now() - start), both);
```

```python tab="Python" tab-group="language"
import asyncio
import time

# wait stands in for a slow call: asyncio.sleep does the waiting
async def wait(seconds: float, label: str) -> str:
    await asyncio.sleep(seconds)
    return label

async def main() -> None:
    # one after the other: wait at the first counter, then at the second
    start = time.perf_counter()
    first = await wait(0.3, "vendors")
    second = await wait(0.3, "invoices")
    print("one after the other", round((time.perf_counter() - start) * 1000), first, second)

    # at the same time: hand in both, then wait for both together
    start = time.perf_counter()
    both = await asyncio.gather(wait(0.3, "vendors"), wait(0.3, "invoices"))
    print("at the same time", round((time.perf_counter() - start) * 1000), both)

asyncio.run(main())
```

```text tab="TypeScript" tab-group="language"
one after the other 602 vendors invoices
at the same time 300 [ 'vendors', 'invoices' ]
```

```text tab="Python" tab-group="language"
one after the other 601 vendors invoices
at the same time 301 ['vendors', 'invoices']
```

Your times will differ by a few milliseconds.

### Which errors are worth a retry, and how often?

Only those that can succeed next time, and only a few times. A `503` or a `429` may succeed on the next try. A `401` or `400` will fail again. Brightline's client retries once on `503` and never on anything else. A client that retries forever sends request after request while a server is down.

### How long should a call wait?

Every call needs a **deadline**, a timeout on the whole call. A call with no timeout can wait forever, and the program waits with it. Brightline's client gives every call a two-second deadline, in both languages.

### What if I forget `await`?

It depends on the language. Both mistakes are common in agent code, so check that every `async` call is awaited.

| Language | What happens without `await` |
| --- | --- |
| TypeScript | The call starts as soon as you make it, and `await` only waits for it. Forget `await`, and the call happens, but nobody waits for its result or its error. You handed in the suit and never went back. |
| Python | A coroutine does nothing until it is awaited or handed to `asyncio.gather`. Forget `await`, and the call never happens. You only filled in the order. Python warns that the coroutine was never awaited. |

### Try it

The file in `14-7` runs two waits each way. Run it and investigate, as in [13.1](../programming-fundamentals/you-read-the-agent-writes.md#try-it).

**Modify.** Send: "Please add a third wait of the same length to both halves of the file. Don't run it yet." Predict both times. Then ask the agent to run it.

**Make.** Decide whether a client should try again after a 400, a 401, a 429 and a 503. Then start the sample API, and send: "Please make a new file in 14-7 called mine that asks the sample API for /vendors with the key from .env, tries again only as my rule says, gives every call a two-second deadline, and prints the status code of every try. Don't run it yet." Paste your rule. Read the lines that decide whether to try again, and then ask the agent to run it. Then ask it to stop the API.

**What you should see.** After your change, about 900 milliseconds one after the other, and still about 300 at the same time. In `mine`, a `503`, then a `200` if your rule tries a 503 again, because the API's first `/vendors` answer is a 503 on purpose.

**The operating rule:** run slow calls at the same time only when they do not depend on each other. Retry only the errors that can succeed next time, and only a few times, and give every network call a deadline.

![Figure 14.8. Two timelines at 300 milliseconds per call. In sequence, a vendors call that gets 503, its retry and three invoice pages take 1.5 seconds. At the same time, the two vendor calls run beside the three pages, so the job takes 0.9 seconds, the time of the pages. Measured, it takes about one second.](img/sequence-vs-together.png)

*Figure 14.8. One after the other, or at the same time. In Brightline's client the pages run in turn, which is simpler. The vendors do not wait for them.*

**Next.** Companies publish code that does all of this for you. [14.8](reading-sdk-code.md) shows how to read it.
