async/await is syntactic sugar — but knowing what it desugars to explains every surprising behavior you encounter. Why does a resolved Promise run before setTimeout(fn,0)? Why does await never truly block? How does the engine know where to resume execution? Understanding the runtime makes you a better async programmer.
⚡ TL;DR: async function returns Promise immediately. await suspends current function, saves stack frame, releases to event loop. Promise callbacks are microtasks — run before next macrotask. setTimeout is a macrotask. V8 uses coroutines for await resumption. Microtask queue drains completely before any macrotask.
async/await desugared — what it really is
// This async/await code:
async function fetchUser(id) {
const response = await fetch('/api/users/'+id);
const user = await response.json();
return user;
}
// Is equivalent to this generator + Promise chain:
function fetchUser(id) {
return new Promise((resolve,reject) => {
const gen = (function*() {
try {
const response = yield fetch('/api/users/'+id);
const user = yield response.json();
resolve(user);
} catch(err) { reject(err); }
})();
// Drive the generator forward on each yield resolution
function step(value) {
const result = gen.next(value);
if(result.done) return;
result.value.then(step,reject);
}
step();
});
}
Event loop phases and microtasks
// Phase order:
// 1. Call stack (synchronous code)
// 2. Microtask queue: Promise callbacks, queueMicrotask()
// 3. Macrotask queue: setTimeout, setInterval, I/O
// Microtask queue DRAINS COMPLETELY between macrotasks
console.log('1'); // Call stack
setTimeout(()=>console.log('2'),0); // Macrotask queue
Promise.resolve().then(()=>console.log('3')); // Microtask queue
Promise.resolve()
.then(()=>console.log('4'))
.then(()=>console.log('5')); // Two microtasks chained
console.log('6'); // Call stack
// Output: 1, 6, 3, 4, 5, 2
// 3,4,5 = microtasks ALL drain before macrotask '2'
Why await never blocks
// await pauses the FUNCTION but not the thread
async function main() {
console.log('A'); // Runs immediately
const data = await fetch('/api/data'); // Suspends main()
// V8 saves the stack frame
// Event loop continues!
console.log('C'); // Resumes after fetch resolves
}
main();
console.log('B'); // Runs WHILE main is suspended
// Output: A, B, C
// JavaScript is single-threaded — await releases control
// Node.js event loop can process other I/O while waiting
Microtask starvation — the hidden danger
// Infinite microtask loop starves event loop!
function bad() {
Promise.resolve().then(bad); // Creates endless microtasks!
}
bad();
// Event loop never processes I/O, timers, or anything else!
// setTimeout callbacks never run
// Same problem with recursive async:
async function flood() {
while(true) await Promise.resolve(); // Blocks event loop!
}
// Fix: use setImmediate() to yield to event loop occasionally
async function better(items) {
for(let i=0; isetImmediate(r)); // Yield!
}
}
- ✅ Promise microtasks execute before setTimeout macrotasks
- ✅ Microtask queue drains completely before next macrotask
- ✅ await suspends function frame, releases thread to event loop
- ✅ async function always returns a Promise (even if you return a value)
- ❌ Never create infinite microtask chains — starves event loop
- ❌ Never mix callbacks and promises in same function
External reference: MDN Microtask guide.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
