Back to JavaScript
JavaScript
medium
mid

How does JavaScript handle asynchronous operations and what mechanisms does it use?

JS is single-threaded; async work uses the event loop. Mechanisms: Web APIs / Node libuv (timers, I/O, network) → task queues (macrotasks for setTimeout, I/O; microtasks for Promises, queueMicrotask). Event loop picks one task → drains microtasks → renders → repeat. Workers give true parallelism. AbortController for cancellation.

5 min read·~10 min to think through

Single-threaded language, async runtime

JS itself runs on one thread. Async happens because the host (browser, Node) offloads work to other threads/processes (network stack, timer queue, file system) and notifies JS when results are ready via the event loop.

The pieces

ts
   [ Call stack ]   ← synchronous code

   [ Event loop ]   ← single coordinator

   [ Microtask Q ]  ← Promise callbacks, queueMicrotask, MutationObserver
   [ Macrotask Q ]  ← setTimeout, setInterval, I/O, postMessage
   [ Render queue ] ← rAF, paint, style/layout (browser only)

   [ Web APIs / libuv ]  ← timers, fetch, fs, network

How a tick proceeds

  1. Pop one macrotask, run to completion.
  2. Drain the microtask queue (run all microtasks queued during step 1, plus any they enqueue).
  3. (Browser) Style/layout/paint if needed.
  4. Repeat.

Implication: a chain of .thens runs entirely between two setTimeouts.

Mechanisms

Promises

The standardized async primitive. Resolved/rejected state, .then queues a microtask.

async/await

Sugar over Promises. Each await schedules continuation on the microtask queue.

setTimeout / setInterval

Macrotask after a minimum delay. Clamped to ~4ms minimum for nested timeouts in browsers.

I/O callbacks (Node) / fetch resolution

Network completes off-thread; callback queued as a (mostly) macrotask in Node libuv phases (timers, pending, poll, check, close).

queueMicrotask

Manually enqueue a microtask — useful for "run after current sync code but before next macrotask."

requestAnimationFrame

Browser-only. Callback runs before paint, ~60Hz on standard displays. Best for visual updates.

requestIdleCallback

Callback runs when the browser is idle. For non-urgent background work.

MessageChannel / postMessage

Cross-thread messaging (Workers, iframes). Also useful for fast macrotask scheduling (MessageChannel is the canonical "schedule task ASAP" hack).

Web Workers / Worker Threads

True parallelism. Separate heap, communicate via postMessage.

SharedArrayBuffer + Atomics

Shared memory between Worker threads with atomic operations.

AbortController

Cancel an in-flight async operation (fetch, etc.).

Microtask vs macrotask order

js
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 1, 4, 3, 2 — microtask before next macrotask

Long-running work problems

JS is single-threaded, so a long sync loop blocks rendering AND further async resolution. Either:

  • Yield (split with await + setTimeout).
  • Web Worker (offload).
  • Suspend non-urgent work (startTransition, requestIdleCallback).

Node specifics

Node's event loop has phases (timers, pending callbacks, poll, check, close). process.nextTick is a Node-specific microtask that fires before Promise microtasks within the same phase. Avoid in new code; prefer queueMicrotask.

Interview framing

"JS is single-threaded; async comes from the host offloading to other threads and dispatching callbacks via the event loop. Tasks split into macrotasks (setTimeout, I/O) and microtasks (Promises, queueMicrotask). The loop runs one macrotask, drains all microtasks, optionally paints, repeats. Promises and async/await are sugar over the microtask queue. For true parallelism use Workers. For cancellation, AbortController. The classic ordering: sync code → microtasks → next macrotask, which is why a Promise.then prints before a setTimeout(0)."

Follow-up questions

  • Walk me through the event loop order with a tricky example.
  • What's the difference between microtask and macrotask?
  • How does Node's event loop differ?

Common mistakes

  • Treating setTimeout(0) as 'immediate' — microtasks run first.
  • Blocking the event loop with long sync work.
  • Mixing nextTick and microtasks.

Performance considerations

  • Anything > 50ms on the main thread blocks interaction. Use Workers for heavy compute, transitions for non-urgent renders.

Edge cases

  • Microtask infinite loop starves rendering.
  • Nested setTimeout clamps to 4ms.
  • process.nextTick ordering in Node.

Real-world examples

  • React 18 startTransition, web-vitals INP measurement, Node event loop debugging.

Senior engineer discussion

Seniors describe microtask vs macrotask ordering precisely, identify event-loop starvation in PR review, and reach for Workers when CPU work justifies it.

Related questions