JavaScript is Single-Threaded

JavaScript runs on a single thread — meaning it executes one statement at a time, in order, from top to bottom. There is no parallel execution inside the JS engine itself.

But we interact with browsers every day that fetch data, run timers, and respond to clicks — all without freezing. How? JavaScript offloads slow work to the browser's Web APIs, and the Event Loop decides when to bring results back into JS execution.

The Call Stack

The call stack is where JavaScript tracks which function is currently running. When you call a function, it gets pushed onto the stack. When it returns, it gets popped off.

function greet(name) {
  return 'Hello, ' + name;
}

function main() {
  const msg = greet('Jaydeep');
  console.log(msg);
}

main();

// Call stack at peak:
// [ main ] → [ greet ] (greet runs, returns, pops off)
// then console.log runs, pops off
// then main returns, stack is empty

The call stack has a size limit. Recursive functions that never stop will cause a Stack Overflow error — "Maximum call stack size exceeded".

Web APIs

Web APIs are features provided by the browser (not the JS engine). When you call setTimeout, fetch, or addEventListener, the browser handles the actual work outside the JS thread.

  • setTimeout / setInterval — timer handling
  • fetch / XMLHttpRequest — network requests
  • addEventListener — DOM events
  • geolocation, localStorage — browser features

When the Web API finishes (e.g. the timer fires, or the response arrives), it does NOT immediately run your callback. Instead it puts the callback into a queue.

The Callback Queue (Task Queue)

The callback queue (also called the task queue or macrotask queue) holds callbacks from Web APIs that are ready to run. Examples: setTimeout callbacks, setInterval callbacks, DOM event handlers.

Callbacks wait in this queue until the call stack is completely empty. They cannot interrupt code that is already running.

The Event Loop

The Event Loop is a continuously running process that does one job: check whether the call stack is empty. If it is, it takes the first item from the queue and pushes it onto the stack to run.

console.log('1 — start');

setTimeout(function() {
  console.log('3 — timeout callback');
}, 0);

console.log('2 — end');

// Output:
// 1 — start
// 2 — end
// 3 — timeout callback

Even with 0ms delay, the setTimeout callback runs after the current synchronous code finishes — because it must wait for the call stack to clear.

Microtask Queue — Promises Run First

There are actually two queues. The microtask queue has higher priority than the callback queue. It contains:

  • Promise.then / .catch / .finally callbacks
  • queueMicrotask()
  • MutationObserver callbacks

After every task from the call stack completes, the Event Loop drains the entire microtask queue before picking the next item from the callback queue.

console.log('1 — start');

setTimeout(() => console.log('4 — setTimeout'), 0);

Promise.resolve()
  .then(() => console.log('2 — promise then'))
  .then(() => console.log('3 — promise then 2'));

console.log('5... wait, no:');

// Actual output:
// 1 — start
// 5... wait, no:   ← synchronous runs first
// 2 — promise then ← microtasks drain next
// 3 — promise then 2
// 4 — setTimeout   ← callback queue runs last

Important: If you keep adding microtasks inside microtask callbacks (e.g. a .then that returns another Promise), the microtask queue will never empty and the callback queue will be starved — the UI can freeze.

Why setTimeout(fn, 0) Isn't Instant

setTimeout(fn, 0) is a common trick to defer code to "after the current script finishes". But it doesn't mean "run immediately" — it means "run as soon as the call stack is empty and the microtask queue is drained".

// Common use case: defer DOM update until after render
button.addEventListener('click', () => {
  input.value = '';
  setTimeout(() => {
    input.focus(); // runs after browser has processed the click
  }, 0);
});

Use setTimeout(fn, 0) when you need to defer work to after the browser has had a chance to paint or process events — not for actual timing precision.

Execution Order Summary

PhaseWhat RunsPriority
Synchronous codeAll top-level statements, function callsFirst (always)
Microtask queuePromise callbacks, queueMicrotaskSecond — drains completely
Callback queuesetTimeout, setInterval, DOM eventsThird — one at a time
RenderBrowser paints changes to screenBetween callback queue tasks

Mental model: Think of the Event Loop as a waiter. Synchronous code is the kitchen preparing the dish. Microtasks are the garnish added before serving. The callback queue is the next table's order — it only gets attention once the current table is fully served.