Skip to content
AtomicReps

Read await as a pause, not a wait

A lesson from The Event Loop You Stopped Drawing. Play it above, or read it through below.

  • An async function runs synchronously to its first await, then tears

    The common read: calling an async function defers its whole body; nothing inside it runs until the event loop gets around to it later.

    It's a reasonable thing to believe: you write the word async on a function, and it's tempting to read that as "this whole thing happens off to the side, later." It doesn't. Calling a() pushes a real frame onto your call stack, the same stack a plain function call would use, and everything in a() runs on it exactly like ordinary code, right up until the function hits an await.

    That's the tear. At await, the runtime pops the function's frame off the stack, and the code that comes after the await stops being "running now." It becomes a continuation: a chip that goes into the microtask queue you already have a name for. The function hasn't finished. It's paused mid-body, and the rest of it is now somebody else's turn to run, once the stack is clear and the queue gets checked.

    That's why a1 logs before main: a() is a real call, running on your stack like any other. And it's why a2 logs after main: the tear moved it into the queue, and the queue only runs once the stack is completely empty. Read this as one rule: sync until the first await, then tear.

  • Await is a scheduling point, not a check

    The common read: the promise is already resolved, so await continues synchronously, without actually pausing anything.

    Lesson 3 gave you two lanes for scheduled work: the task queue and the microtask queue. So which lane does fn()'s continuation land in? You already know the two, and you already know a promise callback is one of them. await doesn't inspect the promise and decide whether to bother pausing. It always parks the rest of the function as a continuation, and that continuation always lands in the microtask queue, whether the promise settled a millisecond ago or hasn't settled yet.

    Read honestly at the mechanism level, await p behaves like handing the rest of the function to p as a .then callback: p.then(v => { / rest of fn / }). That callback doesn't run the instant you write it, even when p is already fulfilled. It runs on its turn in the queue, same as any other promise callback lesson 3 already named.

    That's the whole shape of S4's result: 1 and 3 are both plain synchronous code, so they run first, in source order. 42 only comes back once the stack is empty and the queue is checked, because that's where the tear put it, regardless of how long p had already been sitting there resolved. [MDN: await reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await); the underlying rule is the ECMAScript Await abstract operation.

  • Two awaits, two tears

    The common read: an async function only pauses once, so once it resumes after its first await, the rest of it runs straight through even past a second await.

    There's nothing special about the number one. A function tears at every await it reaches, not just the first. Resuming from one tear doesn't use up the function's ability to tear again; it just means the function is back to running synchronous code, on the stack, until the next await stops it.

    S7's result is carry-in's "newcomers included" law happening in front of you. When the queue drains twoTears()'s first continuation, that continuation runs code, and that code schedules two more things before it's done: the side callback, then the second tear's own continuation. The checkpoint doesn't hand control back to check on those; it keeps pulling from the queue until it's empty. side goes first because it was queued first, and only then does the second continuation run and log t3.

    Count the awaits and you know how many times a function tears. Each tear is its own hop through the queue, and whatever else got queued in between runs in the order it was queued, not the order you'd guess from reading the function top to bottom.

  • What the tear buys you

    An async function is not a thread. It doesn't run on some other core while your code keeps going. It's not deferred, either: the part before the first await runs the moment you call it, on your stack, same as anything else. And it's not parallel: two async functions called back to back still run their synchronous prefixes one after the other, in the order you called them, never at the same time.

    What it is: a function with scheduled seams, one at every await, and each seam is a real handoff to the microtask queue, not a pause button that stops the whole engine.

    Every interleaving question in this course from here on gets decided at those seams. Trace where a function's awaits are, remember that each one tears regardless of how fast the awaited thing settles, and you can read the order off the code without running it.

    | Snippet shape | Order | Why | | --- | --- | --- | | two functions, one await each | a1, b1, main, a2, b2 | both tear before the caller's own last line, both resume after it | | await on an already-settled promise | 1, 3, 42 | the tear happens regardless of settlement | | one function, two awaits | t1, after, t2, side, t3 | each await is its own hop; newcomers still drain first |

    Autocomplete is happy to type the word await for you. It will not tell you which turn of the queue that word just booked.