Callbacks and the Event Loop
Understand synchronous vs asynchronous execution, callback patterns, and how the event loop schedules work after I/O completes.
JavaScript runs user code on a single main thread in browsers and Node — yet it handles thousands of concurrent network requests. That works because long-running operations (timers, fetch, file reads) are delegated to the host environment, and callbacks run later when results are ready. Understanding this model prevents the classic mistakes: blocking the thread, expecting synchronous network code, and callback hell in legacy codebases.
Synchronous vs asynchronous code
Synchronous code runs line by line — each statement finishes before the next begins. Asynchronous code starts work and continues without waiting for the result. The result arrives later via a callback, promise, or await. UI stays responsive and servers handle other requests because the main thread is not stuck waiting on disk or network latency.
A
B
C — timeoutCallbacks — functions called later
A callback is a function you pass to another function to run when async work completes (or on each event). Node's fs.readFile(path, callback), addEventListener('click', handler), and array.forEach all use callbacks. Error-first Node style passes (err, data) so you check err before using data — a convention promises later formalized differently.
Got user: User 1Call stack, task queue, and event loop
The call stack runs synchronous functions LIFO-style. When setTimeout or fetch completes, the host queues a callback as a macrotask (or microtask for promises). The event loop checks: if the stack is empty, dequeue and run the next task. That is why console.log after setTimeout runs before the timeout callback even with delay 0 — the stack must clear first.
1 sync
2 sync
3 microtask
4 macrotask| Concept | Role |
|---|---|
| Call stack | Currently executing synchronous frames |
| Web/Node APIs | Timer, network, fs — async work off main thread |
| Microtask queue | Promise .then, queueMicrotask — runs after stack, before macrotask |
| Macrotask queue | setTimeout, setInterval, I/O callbacks |
Never block the main thread
A long synchronous loop (crunching megabytes of JSON, heavy crypto without workers) freezes the browser tab and stalls Node request handling. Offload CPU-heavy work to Web Workers, worker_threads in Node, or background jobs. Async I/O does not make CPU work async — only waiting work.
The event loop model explains why await does not block other requests in Node and why long synchronous loops freeze browser tabs. When profiling jank, look for sync work on the main thread first — only then optimize promise chains or microtask ordering.
- 1Sync code runs to completion on the call stack
- 2Async APIs invoke callbacks later via the event loop
- 3Microtasks (promises) run before macrotasks (setTimeout)
- 4Avoid deep callback nesting — promises come next
- 5Next lesson: Promise states and chaining