JavaScript runs one call stack but stays responsive by delegating I/O to the host (browser or libuv in Node) and resuming via callbacks, Promises, and async/await.
Core concepts
- Call stack vs task queues: Synchronous code runs to completion; async callbacks queue for later turns. — MDN: The event loop
- Microtasks:
Promise.then,queueMicrotask— run before the next macrotask. — MDN: Microtask guide - Macrotasks:
setTimeout,setInterval, I/O callbacks. — MDN: setTimeout - async/await: Syntax over Promises — still concurrent, not parallel threads. — MDN: Using promises
console.log('A');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('microtask'));
console.log('B');
// A, B, microtask, timeout- fetch & errors:
await fetch(url)— always handle non-OK HTTP statuses explicitly. — MDN: Using Fetch
Concept videos
YouTube lessons for these topics. On a large screen they open in the reading panel next to the roadmap, same as the article links on each core concept.
- Call stack & event loop — Event Loop (Philip Roberts, JSConf EU)
- async/await — JavaScript Crash Course — async section (Traversy Media)
Explanation
Picture a single chef in a kitchen (the call stack). The chef must finish the current dish before starting another. But the restaurant also has a ticket rail (task queues) for timers, network responses, and promise reactions. The event loop is the maître d’ who says, “Chef, when you are free, here is the next ticket.”
Call stack vs task queues
Synchronous code runs straight through — no interruptions. When you call setTimeout, you are not sleeping the chef; you are pinning a future ticket to the rail. When the stack empties, the loop delivers waiting work. That is why setTimeout(fn, 0) still runs after current synchronous lines finish.
Microtasks
Promise reactions are VIP tickets: after each chunk of synchronous work, the loop drains the entire microtask queue before taking the next macrotask. That ordering explains the classic log sequence A, B, microtask, timeout. Microtasks keep promise chains consistent but can starve the UI if you enqueue millions of them in a loop.
Macrotasks
Macrotasks include timers and I/O completion. They represent coarser scheduling. Browser rendering often happens between macrotasks, which is why heavy synchronous JS blocks animations — the chef never looks up at the dining room.
async/await
async/await is storytelling syntax for promises. await pauses your function until a promise settles, but it does not block the whole program — other events still run. You still have one thread; you just write code that reads top-to-bottom like a recipe instead of nested .then ladders.
fetch & errors
fetch resolves even on HTTP 404 — the network succeeded, the server said “not found.” Production code checks response.ok or status codes before parsing JSON. Treat failed HTTP as errors with clear messages so dashboards and users see actionable failures, not silent undefined data.
What Is It?
JavaScript runs user code on one call stack per thread. Long synchronous work freezes the UI in browsers and stalls servers in Node.
Promises represent a future value; async/await flattens Promise chains into readable linear code.
The event loop schedules macrotasks (timers, I/O callbacks) and microtasks (Promise reactions) after the stack clears.
Why Should I Care?
Most production bugs in JS backends and frontends are timing bugs: race conditions, forgotten await, or blocking the loop.
Framework data loaders and React Server Components still bottom out on Promises and fetch — this chapter explains what they assume.
Real World Analogy
The event loop is a restaurant with one cook (the call stack) and a ticket rail (task queues). The cook never chops vegetables while waiting for the oven — they start the oven, take the next ticket, and flip finished orders when they ding.
Visual Mental Model
call stack
|
microtask queue (Promises)
|
macrotask queue (setTimeout, I/O)
|
loop: drain stack -> microtasks -> one macrotaskBeginner Explanation
async/await is syntax on top of Promises. I/O and timers finish later; your job is to not block the single thread while waiting.
Smallest Possible Example
async function main() {
const r = await Promise.resolve(42);
console.log(r);
}
main();Common Mistakes
❌ Forgetting await on an async function.
You log a Promise object instead of data, and errors become unhandled rejections.
❌ Using await inside forEach with async callbacks.
forEach ignores async return values. Use for...of or Promise.all.
❌ Blocking the loop with huge synchronous JSON.parse on megabyte payloads.
Move heavy CPU work to workers or chunk processing so timers and I/O can breathe.
Real Project Example
Cloudflare Workers and Vercel Functions rely on non-blocking I/O to serve many requests per isolate.
Twitter/X feeds and Spotify playlists fetch paginated APIs with Promise chains you will maintain.
Build This Mini Project
Write three functions: sync sleep simulation (bad), setTimeout callback, and async/await fetch to a public API. Log order of prints and sketch the loop on paper.
Interview Question
What runs first: a Promise .then or setTimeout(..., 0)?
After synchronous code finishes, the engine drains the microtask queue (Promise reactions) before taking the next macrotask from the timer queue. So a .then runs before a zero-delay timeout.
Key Takeaways
- One thread — do not block it.
- Promises and async/await are the default async style.
- Microtasks beat macrotasks between turns.
- Always handle rejection paths.
Resources
The 60-second story
One cook. One stove. A dining room full of people who will leave if you stare at the oven for twenty minutes. You put the pizza in, hand the customer a receipt, and go take the next order. When the oven beeps, you do not drop the plate you are holding. You finish that plate, then you look at the receipts that are ready.
That cook is the call stack. JavaScript does one thing at a time on that stove. The oven, the delivery app, and the card reader are the browser or Node doing slow work off to the side. When they finish, they do not shove the cook. They leave a ticket. The event loop is the manager who decides which ticket the cook picks up next, and only when the stove is clear.
Two kinds of tickets matter.
Microtasks are the “this receipt is part of the order you just promised” pile. Promise .then callbacks and queueMicrotask sit there. They run before the cook walks back to the dining room.
Macrotasks are the “check the oven in a bit” pile. setTimeout, setInterval, and a lot of I/O land there. A setTimeout(fn, 0) is not “run this now.” It is “run this after you finish the current plate and the small receipt pile.”
Map it.
- The cook’s hands = the call stack.
- The manager = the event loop.
- The small urgent receipt pile = the microtask queue.
- The “later” pile = the macrotask queue.
- The pizza receipt = a Promise.
- The oven failing = a rejected Promise.
async/await= writing the wait in normal sentences instead of a chain of receipts taped together.fetch= calling another restaurant and still having to check whether they actually cooked the food. A response can arrive and still be a 404.
console.log('take order');
setTimeout(() => console.log('oven beep'), 0);
Promise.resolve().then(() => console.log('receipt update'));
console.log('next table');You get take order, then next table, then receipt update, then oven beep. The cook finishes the current function first. Then the Promise receipt. Then the timeout. If that order surprises you, you do not have a bug yet. You have the whole plot.
Promises were invented because the cook cannot freeze the restaurant while the oven runs. Callbacks alone turned into receipts stapled to receipts until nobody could see the order. async/await is still that receipt system. It just reads like a story. It does not hire a second cook. Parallel threads are a different kitchen.
await fetch(url) only means the response showed up. It does not mean the status was OK. Check response.ok the way you check that the box actually contains a pizza.
Mental model: one cook, a manager, two ticket piles. JavaScript is incredibly fast at doing other work while it waits. It is not doing your slow job and your click handler at the same instant on that one stove.
Say it out loud once: current function finishes, microtasks run, then macrotasks. Await is a readable Promise. Fetch can succeed at the network and still fail as an HTTP status.


