await does block — just not where you think
Every tutorial says async/await is "non-blocking." But look at this:
async function getData() {
const data = await fetch('/api');
console.log(data); // this line waits
}console.log(data) cannot run until fetch finishes. That is waiting. That is blocking. So why does everyone keep saying it's not?
Because "non-blocking" refers to the caller, not the next line.
The confusion
The word "blocking" gets used without saying what is being blocked. Inside the function, await absolutely blocks the lines below it. The function pauses and the remaining code waits.
But step outside:
getData();
console.log('This runs right away');console.log fires immediately. It does not wait for fetch. The caller moved on. The main thread kept going.
So await blocks the function it lives in. It does not block whoever called that function — unless the caller also uses await.
Nested await
Three layers, each with await:
async function fetchData() {
console.log('3. fetchData: before await');
await delay(500);
console.log('3. fetchData: after await');
}
async function process() {
console.log('2. process: before await');
await fetchData();
console.log('2. process: after await');
}
async function main() {
console.log('1. main: before await');
await process();
console.log('1. main: after await');
}
main();
console.log('0. main thread: immediate');Output:
1. main: before await
2. process: before await
3. fetchData: before await
0. main thread: immediate ← not first, but before the 500ms delay
[resolved] fetchData (500ms)
3. fetchData: after await ← innermost resolves first
2. process: after await
1. main: after await ← outermost resolves last
main thread: immediate is not the first line to print. Each function enters and runs synchronously until it hits its await. So main calls process, process calls fetchData, fetchData hits await delay(500) — only then does execution suspend and return to the caller. An async function behaves like a regular function call until the moment it actually awaits something.
After 500ms, the await chain unwinds inside-out, like a call stack. main() was called without await, so the main thread never waited.
What if the top level also awaits?
Change main() to await main():
await main();
console.log('0. main thread: immediate');1. main: before await
2. process: before await
3. fetchData: before await
[resolved] fetchData (500ms)
3. fetchData: after await
2. process: after await
1. main: after await
0. main thread: immediate ← now it waits too
immediate moves to the end. The top level used await, so the top level waits too.
Side note: top-level await is ES module syntax. Node detects it and reparses the file as ESM, which is why a MODULE_TYPELESS_PACKAGE_JSON warning appears. await inside an async function works in both CJS and ESM. Only top-level await requires ESM.
So which is it — blocking or not?
Both. await blocks the scope it sits in. Every line after await in the same function waits. Add await to the caller, that caller waits too. The blocking propagates upward, one layer at a time.
The only reason async/await is called "non-blocking" is that this waiting never reaches the main thread — unless someone explicitly awaits all the way up. The function suspends, wraps the rest as a microtask, and hands control back to the event loop. The caller is free to continue.
"Async is non-blocking" is a statement about the main thread. The next line after await is absolutely blocked.