JavaScript, Refreshed
Asynchrony: callbacks → promises → async/await
Lesson 4 of 5
What you'll learn
- Trace why async code runs after the current synchronous code finishes
- Distinguish microtasks (promises) from macrotasks (timers)
- Read
async/awaitas a flatter way to write promise chains
JavaScript runs on a single thread with an event loop. Synchronous code runs to completion first. Anything deferred goes into a queue and runs later, once the call stack is empty. This is why a setTimeout(fn, 0) callback never interrupts the code around it — it waits its turn.
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");
// prints A, C, B — the timer waits until sync code is done
Microtasks vs timers
Not all deferred work is equal. Promise callbacks (.then, and the continuation after await) go on the microtask queue, which the engine drains completely after each chunk of sync code — before it touches any timer. setTimeout callbacks are macrotasks and run later. So a resolved promise always beats a setTimeout(0).
console.log("sync 1");
Promise.resolve().then(() => console.log("microtask"));
setTimeout(() => console.log("timer"), 0);
console.log("sync 2");
// sync 1, sync 2, microtask, timer
async / await
async/await doesn't change this order — it's syntax over promises. An await pauses the function and schedules the rest as a microtask, so the code reads top-to-bottom while still running asynchronously.
await yields a microtask
The lines after an await resume as a microtask, not synchronously. They'll still run before any pending setTimeout, even one queued earlier.
The challenge models the queues with plain arrays so you can see the printing order without any real timers — sync first, then microtasks, then macrotasks.
Run it. We log synchronously, queue microtasks and macrotasks, then drain microtasks fully before macrotasks — matching real engine order.
Next: the modern syntax that makes this code shorter and safer.
Sign in to save your progress across devices.