# Async/Await & the Event Loop

> Async exists because programs spend most of their time waiting; the event loop is a single thread plus a queue of ready-to-continue work; and async/await is just readable syntax over 'a value that isn't here yet.'


---

# Async/Await & the Event Loop

You've written `await` because the tutorial told you to, and it worked - until a function returned `Promise { <pending> }`, or a missing `await` let the next line run too early, or someone said "don't block the event loop" and you nodded along. Async code works until it suddenly doesn't, and then it feels like magic that turned on you.

It isn't magic. Async solves an ordinary problem: waiting is slow, and a worker who stands idle while waiting is a worker wasted. This guide builds the model in order - the problem, then the engine, then the syntax - so `await` stops being a spell and becomes something you can reason about.

We'll use JavaScript for the examples because it has the clearest, most-visible async model - but the underlying idea (waiting is wasteful; let the worker do other things) is universal. Python's `asyncio`, Rust's `async`/`.await`, C#'s `async`/`await`, and Kotlin's coroutines are all the same idea wearing different clothes.

## How to read this
- **Want the one-sentence version?** Async exists so a single worker doesn't sit frozen while waiting on the network, disk, or a timer. Read [Phase 1](01-why-async-exists.md) and you'll have the whole point.
- **Want it to finally make sense?** Read in order. Each phase builds on the last: the *problem* (Phase 1), the *engine* (Phase 2), then the *syntax* (Phase 3).

## The phases
1. **[Why Async Exists](01-why-async-exists.md)** - the problem nobody states plainly: a huge fraction of programming is *waiting*. We compare blocking (stop and wait) against non-blocking (start it, do other work, come back) with a restaurant-waiter analogy and a timeline you can see.
2. **[The Event Loop](02-the-event-loop.md)** - the engine that makes non-blocking work: one thread running your code, plus a queue of "things ready to continue." Why "single-threaded but concurrent" isn't a contradiction, and what "don't block the event loop" actually means.
3. **[Promises & async/await](03-promises-and-async-await.md)** - how the syntax maps to the model. A promise is "a value that isn't here yet"; `await` means "pause *this* function until it's ready, without freezing the loop." Annotated before/after, plus the two gotchas that bite everyone.

> This guide is about the *model* - why async exists and how to read it. Deep operational topics (cancellation, backpressure, parallelism across multiple cores, async streams) are deliberately deferred to a follow-up so this one stays a clean mental model rather than a reference manual.

Related reading: [What Happens When Code Runs](/guides/what-happens-when-code-runs) · [Processes, Memory & the CPU](/guides/processes-memory-and-cpu)


---

# Why Async Exists

Here's the thing nobody says out loud when they teach you to code: **a huge amount of what your program does is wait.** It asks a database for a row and waits. It requests a web page and waits. It reads a file off disk and waits. It sets a timer and - you guessed it - waits.

And waiting, in computer terms, is *slow*. Not slow like "this loop runs a million times" slow. Slow like "the network round-trip to another continent takes a few hundred milliseconds, during which your CPU could have executed hundreds of millions of instructions" slow. The numbers are lopsided: the CPU is a sprinter, and the network, disk, and timers are the post office.

So the real question async answers is: **what should the worker do while it waits?** There are exactly two answers, and the difference between them is the whole story.

## The two ways to wait

📝 **Terminology.** *Blocking* means an operation stops your code dead until it finishes - the line `data = read_file()` doesn't return until the file is fully read, and nothing else runs in the meantime. *Non-blocking* means an operation starts the work and returns immediately, letting your code keep going; you get the result later, when it's ready.

That's the entire distinction. Blocking: stop and wait. Non-blocking: start it, do other things, come back.

## The restaurant analogy

This clicks fastest with a picture you already know: a waiter in a restaurant.

Imagine a waiter - one person, one worker - taking care of several tables. A customer orders. The waiter walks the order to the kitchen. Now the food needs ten minutes to cook. What does the waiter do?

**The blocking waiter** stands at the kitchen window for ten full minutes, staring at the pan, doing nothing, until the food is ready. Then he delivers it, and only *then* walks to the next table to take their order. Your other tables sit there, menus closed, ignored. One slow dish freezes the entire restaurant.

**The non-blocking waiter** drops the order at the kitchen and immediately walks to the next table to take *their* order. While the first dish cooks, he's seating new guests, refilling drinks, clearing plates. When the kitchen rings the bell - "table 4 is ready!" - he picks up the dish and delivers it. The cooking still takes ten minutes. But the waiter was never idle, so the whole room stays served.

Notice what didn't change: the food still takes ten minutes either way. Non-blocking doesn't make the *waiting* faster - it makes the *worker* productive during the wait. People expect async to speed up the slow thing; it doesn't. It stops the slow thing from freezing everything else.

💡 **Key point.** Async doesn't make waiting shorter. It makes waiting *non-exclusive* - one wait no longer holds up all the other work.

## Seeing it on a timeline

Say your program needs to do three things, each of which mostly waits on the network: fetch a user, fetch their orders, and fetch a notification. Each request takes about 100 ms, almost all of it spent waiting for the server to reply.

The blocking version does them one after another, standing at the kitchen window each time:

```mermaid
gantt
  title BLOCKING - waits stack back-to-back (~300 ms)
  dateFormat X
  axisFormat %L
  fetch user    :0, 100
  fetch orders  :100, 200
  fetch notif   :200, 300
```

The non-blocking version starts all three, then handles each reply as it arrives:

```mermaid
gantt
  title NON-BLOCKING - waits overlap (~100 ms)
  dateFormat X
  axisFormat %L
  fetch user    :0, 100
  fetch orders  :0, 100
  fetch notif   :0, 100
```

*What's happening:* Blocking waits stack back-to-back because the worker won't start the second wait until the first finishes. Non-blocking kicks off all three waits up front and collects results as they arrive, so the waits *overlap* instead of stacking - same network, same per-request time, roughly a third of the wall-clock time. (Illustrative round numbers, not a measured benchmark.)

⚠️ **Gotcha.** This overlap only helps when the work is *waiting* (network, disk, timers - often called **I/O-bound** work). If your three tasks were each grinding the CPU at 100% - say, hashing a giant file - async wouldn't help at all, because there's no idle waiting to fill. A single worker can only *compute* one thing at a time. Async fills *waiting* time, not *computing* time. (Filling computing time means using multiple workers - threads or processes - which is a different tool; see [Processes, Memory & the CPU](/guides/processes-memory-and-cpu).)

## A real example

Let's make the difference concrete. Here's blocking code - the kind you'd write without async - fetching two URLs in sequence. Each `fetchSync` call stops the world until its response arrives:

```console
$ node blocking.js
[t=0ms]   start
[t=512ms] got first response
[t=1041ms] got second response
[t=1041ms] done
```
*What just happened:* The program sat frozen ~512 ms waiting on the first request, then only began the second - another ~529 ms. The total is the sum: the second wait couldn't start until the first was completely over. During both waits the CPU had nothing to do - the blocking waiter at the kitchen window.

Now the non-blocking version, which starts both requests before waiting on either:

```console
$ node nonblocking.js
[t=0ms]   start
[t=0ms]   both requests sent
[t=534ms] both responses arrived
[t=534ms] done
```
*What just happened:* Both requests went out at `t=0`, so their waits overlapped. The program finished in roughly the time of the *slower single request*, not the sum of both - we didn't add workers or speed up the network, we just stopped the first wait from blocking the second. (Times vary with your network; the shape - overlap vs. sum - is the point.)

**Why this saves you later.** Once you can see the difference between blocking and non-blocking on a timeline, a whole class of "why is my app so slow?" mysteries dissolves. A web server that handles one request at a time because each one blocks on the database; a UI that freezes solid while it loads data; a script that takes 30 seconds doing ten 3-second waits in a row - these are all the blocking waiter, and you'll recognize him on sight. The fix is almost always: stop standing at the kitchen window.

## Recap

1. **Most programs spend most of their time waiting** - on the network, disk, and timers - and the CPU is wildly faster than any of those, so naive waiting wastes the worker.
2. **Blocking** = stop and wait; nothing else runs until the operation finishes. **Non-blocking** = start the wait, do other useful work, collect the result when it's ready.
3. **The restaurant waiter** is the model: the non-blocking waiter never stands idle, so one slow dish doesn't freeze the whole room.
4. **Async fills *waiting* time, not *computing* time.** It overlaps waits; it does not make a single wait shorter, and it doesn't help CPU-bound work.

So non-blocking is clearly better for waiting - but *how* does one worker juggle many overlapping waits without dropping anything? What rings the bell when "table 4 is ready"? That mechanism has a name, and it's the engine the whole model runs on: the event loop.


---

# The Event Loop

In Phase 1 we left a question hanging: how does *one* worker juggle many overlapping waits without dropping any of them? Who rings the bell when "table 4 is ready," and how does the waiter know to go pick it up?

The answer is a beautifully simple machine called the **event loop** - the engine underneath every `await` you've ever written. Once you can picture it turning, async stops being mysterious. It's two parts: a single worker that runs your code, and a line of jobs waiting their turn.

## What the event loop actually is

**What it actually is.** The event loop is a single thread - one worker - paired with a **queue** of tasks that are ready to run. The loop does one thing forever: take the next ready task off the queue, run it *all the way to completion*, then grab the next one. Run, finish, grab - forever.

📝 **Terminology.** A *thread* is a single sequence of execution - one worker doing one thing at a time, in order. A *queue* here is just a waiting line: jobs join at the back and get picked up from the front. The event loop has one thread and (at least) one queue.

The "events" are the bells from Phase 1: a network response arrived, a timer fired, a file finished loading. When one of those happens, the work that should run *next* (the code waiting on that result) gets placed in the queue. The loop will get to it.

```mermaid
flowchart TD
  Net["network reply"] --> Q
  Timer["timer fires"] --> Q
  File["file loaded"] --> Q
  Q["THE QUEUE<br/>task · task · task<br/>(join at back, picked from front)"] -->|1. take next task| Run["run it to completion<br/>(the single thread)"]
  Run -->|2. go back| Q
```

*What's happening:* Your code runs on the single thread. When it starts a wait - a network request, say - it hands that off (to the OS, the browser, the runtime) and *returns immediately*, freeing the thread for other things. When the reply lands, the runtime drops the "continue here" work into the queue, and the loop eventually picks it up. The waiter dropped the order and walked away; the bell put the finished dish back in his path.

## Single-threaded but concurrent - not a contradiction

This is the line that trips everyone, so let's defuse it directly: JavaScript runs your code on **one thread**, yet it handles **many things at once**. How can both be true?

The trick is distinguishing two words people use loosely:

📝 **Terminology.** *Concurrency* is dealing with many tasks over the same period by interleaving them - making progress on several by switching between them. *Parallelism* is doing many tasks *at the same instant*, which requires multiple workers (multiple CPU cores). They are not the same thing.

The waiter is **concurrent, not parallel.** There is one waiter. He never carries two dishes through the door in the same instant. But over the course of an evening he keeps *many* tables progressing, because each table spends most of its time *waiting* (for food, for the bill), and he fills those gaps by tending other tables. One worker, many tasks in flight, none of them frozen.

That's exactly the event loop. The single thread is the one waiter - it runs one piece of code at a time, truly one. But because each task hands off its waiting and steps aside, the thread is free to advance other tasks during those gaps. The result *looks* like many things happening together, and for waiting-heavy work it's nearly as good, without the cost of multiple threads.

💡 **Key point.** One thread can keep hundreds of waiting tasks moving, because waiting doesn't occupy the thread. The thread is only ever busy during the brief moments of actual *computing* between the waits.

## The catch: the thread can only do one thing at a time

Here's the flip side - and the most important practical fact in this guide. The loop runs each task **to completion** before touching the next one; it can't pause your code mid-calculation to answer a network reply, because it has no way to interrupt a running task. It waits for your task to *finish and return* before picking up anything else.

So if one of your tasks doesn't return for a long time - a giant loop, a synchronous file parse, a heavy calculation - the loop is *stuck inside it*. The queue piles up. Timers don't fire. Clicks don't register. Network replies sit unhandled. The single waiter is trapped in the kitchen doing arithmetic, and the whole dining room waits.

This is the meaning of the warning you've heard:

⚠️ **Gotcha: don't block the event loop.** A long-running *synchronous* task freezes everything, because the single thread can't do anything else until that task returns. In a browser, the page goes unresponsive - no scrolling, no clicks, the dreaded spinning cursor. On a server, *every* incoming request stalls until the one slow task finishes. The fix is to break the long task into chunks that return control to the loop, or hand the heavy computing to a separate worker (a Web Worker in the browser, a worker thread or separate process on a server).

## Seeing the block

Let's watch the loop get stuck. This code sets a timer to fire after 100 ms, then immediately runs a long synchronous loop. You'd hope the timer fires on time:

```console
$ node blockdemo.js
[t=0ms]   timer set for 100ms
[t=0ms]   starting a 2-second synchronous calculation...
[t=2013ms] calculation done
[t=2013ms] timer callback finally ran
```
*What just happened:* The timer was *due* at 100 ms - its "continue here" work sat in the queue, ready, on time. But the loop couldn't pick it up: the single thread was trapped inside the 2-second synchronous calculation, running it to completion. Only when that calculation returned and freed the thread could the loop pull the timer's work off the queue. The timer didn't fire late because the timer was slow; it fired late because *we* blocked the one worker who answers the bell.

**Why this saves you later.** This single picture explains a startling number of real-world bugs. "My web server handles requests fine until one endpoint does heavy work, then *all* requests hang" - blocked loop. "My UI freezes for two seconds when I click Export" - blocked loop. "I added a `console.log` inside a tight million-iteration loop and the whole tab died" - blocked loop. Once you know the loop runs each task to completion on one thread, you stop being surprised by these, and you know the fix: get the heavy work off the one thread that's answering everyone.

## Recap

1. **The event loop** is one thread plus a queue: take the next ready task, run it to completion, go back for the next - forever.
2. **Events fill the queue.** When a wait finishes (network reply, timer, file load), the work that should continue is dropped into the queue for the loop to pick up.
3. **Single-threaded but concurrent** isn't a contradiction: one worker keeps many *waiting* tasks progressing by filling the gaps. That's concurrency (interleaving), not parallelism (simultaneous workers).
4. **The loop runs each task to completion** and can't interrupt it - so a long *synchronous* task **blocks the event loop**, freezing everything until it returns.

We now have the engine. But writing code that hands off waits and picks them back up - using the queue directly - would be a nightmare of nested callbacks. The last piece is the syntax that lets you *write* async code as if it were ordinary top-to-bottom code, while it quietly cooperates with the loop: promises, and `async`/`await`.

Step through exactly what happens - watch the call stack empty, then microtasks drain before the next macrotask:

```playground-eventloop
```

Watch it animated: [the event loop](/explainers/EventLoop.dc.html)


---

# Promises & async/await

You've got the model now: waiting is wasteful (Phase 1), and a single-threaded event loop fills the waits by juggling tasks (Phase 2). What's left is the part you actually *type* - `async`, `await`, and the thing called a *promise*. They don't introduce new ideas; they're a readable skin stretched over the machine you already understand.

## A promise is "a value that isn't here yet"

**What it actually is.** A **promise** (called a *future* in some languages) is an object that stands in for a value you don't have *yet* but will have *later* - the result of a wait. The moment you start an async operation, you don't get the result; you get a promise, a receipt that says "the real value is coming; hold onto this and I'll fill it in."

A promise is always in one of three states:

```mermaid
stateDiagram-v2
  [*] --> Pending: start the wait
  Pending --> Fulfilled: value arrived
  Pending --> Rejected: it failed (error)
  Fulfilled --> [*]
  Rejected --> [*]
```

*What's happening:* A promise starts **pending** - the food is still cooking. It then settles exactly once, into one of two final states: **fulfilled** with the value (the dish is ready), or **rejected** with an error (the kitchen dropped it). Once settled, it never changes again. This is why you sometimes see `Promise { <pending> }` printed: you logged the *receipt* before the value arrived.

📝 **Terminology.** *Pending* = not done yet; *fulfilled* (or "resolved") = succeeded with a value; *rejected* = failed with an error. "Settled" means it reached fulfilled or rejected.

## await means "pause THIS function until it's ready"

**What it actually is.** `await` is the word that unwraps a promise. You put it in front of a promise, and it means: *pause this function right here until the promise settles, then give me the value* (or throw the error, if it rejected).

What `await` pauses - and doesn't - is the crucial part, and it's what makes this work with everything from Phase 2:

💡 **Key point.** `await` pauses **only the function it's written in.** It does **not** block the event loop. The single thread is freed to go run other queued tasks while this function sits paused. When the awaited promise settles, the runtime drops "continue this function" into the queue, and the loop picks it back up.

`await` *looks* like blocking - the code below it doesn't run until the value arrives, just like a blocking call. But underneath it's pure non-blocking: your function steps aside and hands the thread back to the loop during the wait. It's the polite waiter who drops the order and walks away, written so it *reads* like the waiter who stands and waits - blocking-style readability with non-blocking behavior.

📝 **Terminology.** The `async` keyword in front of a function does two small things: it lets you use `await` inside that function, and it makes the function automatically return a promise. So `async function` = "this function does async work and hands back a promise."

## The before and after: callbacks → async/await

Before promises and `await`, you handled "do this when the value arrives" by passing a function to call later - a **callback**. It works, but stack a few in a row and it nests into a sideways pyramid that's miserable to read and harder to get error handling right. Here's the same job - fetch a user, then their orders, then the first order's details - written both ways.

The callback version:

```javascript
getUser(userId, (err, user) => {
  if (err) return handle(err);                 // error handling, attempt 1
  getOrders(user.id, (err, orders) => {
    if (err) return handle(err);               // error handling, attempt 2
    getDetails(orders[0].id, (err, details) => {
      if (err) return handle(err);             // error handling, attempt 3
      console.log(details);                    // finally, the actual point
    });
  });
});
```
*What just happened:* Each step nests inside the previous step's callback, since each one can only start once the one before it delivers its result. The real work - `console.log(details)` - is buried three levels deep, and the error handling is copy-pasted at every level. This rightward drift is the famous "callback pyramid," and it worsens with every step you add.

The same logic with `async`/`await`:

```javascript
async function showDetails(userId) {
  try {
    const user = await getUser(userId);        // pause until user arrives
    const orders = await getOrders(user.id);   // then pause until orders arrive
    const details = await getDetails(orders[0].id);  // then the details
    console.log(details);                      // the point, right where it belongs
  } catch (err) {
    handle(err);                               // one place handles any failure
  }
}
```
*What just happened:* The same three sequential waits now read top-to-bottom like ordinary code. Each `await` pauses the function until its value is ready, and during each pause the event loop is free to do other work, exactly as in the callback version. The nesting is gone, values flow down in plain variables, and a single `try/catch` handles a failure at *any* step (a rejected promise makes `await` throw, so your normal error handling catches it) - same model, same behavior, a fraction of the cognitive load.

⚠️ **Note on sequencing.** Those three `await`s run *one after another* because each genuinely needs the previous result. If steps are *independent*, awaiting them in a row makes them wait in sequence for no reason - that's the blocking-style timeline from Phase 1, reintroduced by accident. To overlap independent waits, start them all first, then await together: `const [a, b] = await Promise.all([fetchA(), fetchB()])`. Reach for that when the tasks don't depend on each other.

## The gotcha: forgetting await

Because an `async` function returns a promise, *calling it gives you a promise, not the value.* If you forget `await`, you're holding the receipt and treating it like the meal.

```console
$ node forgot-await.js
Promise { <pending> }
TypeError: Cannot read properties of undefined (reading 'name')
```
*What just happened:* The code called `getUser(id)` without `await`, so the variable held a pending *promise*, not a user object - logging it showed `Promise { <pending> }`, the receipt printed before the value arrived. Then the code tried to read `.name` off the promise, which doesn't have one, and blew up. This is the most common async mistake: the missing `await` doesn't error on its own line; it quietly hands you a promise, and the crash happens later when you use the "value" that was never unwrapped.

⚠️ **Gotcha.** If a value is `Promise { <pending> }`, or a property is mysteriously `undefined` right after an async call, your first suspect is a missing `await`. The code often *looks* right because it runs without an immediate error - the promise just flows downstream as the wrong type until something tries to use it.

## The other gotcha: unhandled rejections

A promise can reject - the wait failed. If nothing is there to catch that rejection, the error doesn't vanish; it surfaces as an **unhandled promise rejection**, a separate failure mode that's easy to miss because it's not a normal thrown exception in the line of code you're looking at.

```console
$ node no-catch.js
node:internal/process/promises:391
    triggerUncaughtException(err, true /* fromPromise */);
    ^
Error: network request failed
    at fetchUser (/app/no-catch.js:4:9)
[...]
Node.js v20.x
```
*What just happened:* An `async` function's promise rejected - the network request failed - and nothing caught it: no `await` inside a `try/catch`, no `.catch(...)` on the promise. The rejection bubbled up as an *unhandled rejection*. In modern Node.js this crashes the process by default; in a browser it logs an "Uncaught (in promise)" error to the console. Either way, the failure surfaces far from where you'd look, because it escaped your normal error handling.

⚠️ **Gotcha.** Every promise that can fail needs a place to catch the failure: either `await` it inside a `try/catch`, or attach a `.catch()` to it. A "fire-and-forget" async call with no error handling is a rejection waiting to surprise you - often in production, often at 2am.

**Why this saves you later.** These two gotchas - the missing `await` and the unhandled rejection - cause a huge share of real async bugs, and both are quiet: neither errors on the line where you made the mistake. Knowing their *symptoms* (`Promise { <pending> }`, a surprise `undefined`, an "Uncaught (in promise)" from nowhere) turns a baffling debugging session into a five-second diagnosis.

## Recap

1. **A promise is a value that isn't here yet** - a receipt for the eventual result of a wait. It's *pending*, then settles once into *fulfilled* (got the value) or *rejected* (got an error).
2. **`await` pauses only its own function** until the promise settles, without blocking the event loop - the thread is freed to run other queued work during the wait. `async` lets a function use `await` and makes it return a promise.
3. **async/await is readable syntax over the Phase 2 model** - it turns the sideways callback pyramid into top-to-bottom code with one `try/catch`, while behaving identically underneath.
4. **The two gotchas bite everyone:** forgetting `await` hands you a `Promise { <pending> }` instead of the value, and an uncaught rejection surfaces far from where it happened. Both are quiet - learn their symptoms.

That's the whole arc: waiting is wasteful, the event loop fills the waits, and `async`/`await` is the readable syntax for writing non-blocking code top-to-bottom. Next time `await` surprises you, you'll be reasoning about a machine you can see - not staring at a spell.

> Want to go deeper? Cancellation, running work across multiple cores, async streams, and backpressure build on this foundation - those are a follow-up guide. With the model in this guide, you have what you need to read and reason about the async code in front of you today.

Related reading: [What Happens When Code Runs](/guides/what-happens-when-code-runs) · [Processes, Memory & the CPU](/guides/processes-memory-and-cpu)

Watch it animated: [async/await](/explainers/AsyncAwait.dc.html)
