skip to content

Event Loop and Concurrency

How JavaScript stays responsive on a single thread: one call stack, separate queues of pending jobs, and a loop that decides what runs next. Interviewers lean on this pillar because 'what does this log, and in what order?' cleanly separates people who memorized async syntax from people who understand the scheduler underneath it.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

JavaScript engines run your code on a single call stack. What gets pushed onto and popped off that stack, and why does that mean two of your functions can never be executing at the same instant?

level: juniorimportance: must knowfreq 78%

answer

  1. one stack per agent
  2. a frame per in-progress call
  3. calls push, returns pop
  4. only the top frame is running
  5. callbacks always begin on an empty stack

basics

~20 s

The call stack holds one frame per function call that has started but not returned — its arguments, its locals, and the point to resume at. Calls push a frame, returns pop it, and because JavaScript has a single stack, only the top frame is ever running.

solid answer

~40 s

The call stack is the engine's record of the calls that have started but not yet finished. Each call pushes a frame holding that call's arguments, its local variables, and where to resume when it returns; each return pops the frame. Because a JavaScript agent has exactly one stack, only the frame on top is actually executing, so two of my functions can never be mid-execution simultaneously and nothing gets slipped in between two of my statements. Asynchronous work does not break this: a `setTimeout` or `fetch` call returns immediately and its frame pops, the host does the waiting outside JavaScript, and the callback later runs as its own job starting from an empty stack. So "single-threaded" here really means one stack — and whatever holds that stack holds the whole runtime.

go deeper

for a junior

Be ready to say that each call adds a frame and each return removes it, and that JavaScript has one such stack, so your functions never run at the same moment.

for a middle

Explain what a frame actually holds — arguments, locals, and the resume point — and trace a small nested-call example, naming exactly which frames are live when the innermost line executes.

for a senior

Show why it matters operationally: whatever holds the stack owns the entire runtime, so one slow synchronous call has runtime-wide latency effects and shows up in a CPU profile as a single unbroken stack.

for a principal

Own the design consequence: with one stack per agent, the amount of synchronous work permitted per job becomes a system-level budget, and any component capable of holding the stack for long needs architectural separation rather than tuning.

## What the call stack is When a function is called, the engine must remember three things: the arguments it was called with, the local state it builds up while running, and where execution should resume once it returns. It packages that into a **frame** (in specification language, an execution context) and places the frame on a stack — a last-in, first-out structure. A call pushes a frame; a return pops it. The stack is a fixed-size memory region the engine manages for you; you never allocate or address it directly, you only observe its shape through debuggers, profilers and error stack traces. ## Push and pop, in strict order ```js function inner() { console.log('inner running'); } function outer() { inner(); console.log('outer resumes'); } outer(); ``` At the moment `console.log('inner running')` executes, three frames are live: `outer` at the bottom, `inner` above it, and `console.log` on top. `console.log` returns and pops; `inner` reaches its end and pops; control resumes inside `outer` exactly where it left off, because that resume point was stored in `outer`'s frame. Nothing else can be interleaved between those pops — the order is fully determined by the call structure. ## One stack per agent ECMAScript models a running program as an **agent**: one thread of execution, one job queue, one call stack. Within an agent, exactly one frame — the topmost — is executing at any instant. There are only two ways a different function starts running. Either the currently running function calls it, in which case its frame goes *above* the caller's and the caller is suspended in the ordinary sense (it is still on the stack, waiting for the callee to return); or the stack drains completely and the runtime picks up the next queued job, which begins on an empty stack. That second path is the important one. Every callback you ever write — a timer callback, a promise reaction, an event handler — starts at the bottom of an empty stack. It never appears halfway up somebody else's stack. This is why you can reason about a function's body as an uninterrupted unit. ## Then how does anything happen "in the background"? A single JavaScript stack does not imply the *host* is single-threaded. When you start a timer or a network request, the JavaScript call that starts it does almost nothing and returns immediately; its frame pops. The waiting, the socket work, the disk read — those are carried out by the host outside the JavaScript stack, quite possibly on other operating-system threads. When the work finishes, the host queues a job, and your callback runs later on a fresh stack. So the parallelism is real, but it is parallelism of *host work*, not of your JavaScript. Only one piece of your code ever holds the stack. ## What follows from it - **A function that never returns owns the runtime.** Its frame stays on the stack, the stack never empties, and no queued job can start. That is the whole mechanism behind a frozen page. - **You never observe half-updated state from a callback.** A callback cannot begin until the current stack is gone, so it can only ever see state as of a completed statement sequence. - **Frames also pop on `throw`.** An exception unwinds frames one by one until it finds one with a matching `catch`. If none exists, the job ends with an empty stack, the error is reported as uncaught, and the runtime moves on to the next job. - **Depth is finite.** Each live call consumes part of a fixed region, so nesting calls deeply enough exhausts it and the engine throws — the everyday sign that frames really are physical, not conceptual. ## Common misreadings The most frequent one is treating the stack as a queue: "my callbacks are sitting on the stack waiting." They are not — queued callbacks live in job or task queues, and the stack holds only calls currently in progress. The second is inferring extra threads from asynchronous syntax: `async` functions, promises and timers all run their JavaScript on the same single stack; nothing about them starts a second interpreter. The third is imagining the engine can pause your function to squeeze in an urgent callback. It cannot — there is no preemption point between your statements, which is exactly why JavaScript code needs no locks around ordinary object mutation.

  • If only one function runs at a time, how can a network request be in flight while my code keeps running?
    The JavaScript call that starts the request returns immediately and its frame pops; the transfer itself is performed by the host, outside the JavaScript stack and possibly on another thread. When it completes, the host queues a job and your callback runs later on a fresh stack. Only JavaScript execution is serialised — not the work the host performs on your behalf.
  • What happens to the stack when a function throws instead of returning?
    The throw unwinds the stack, popping frames one at a time until it reaches a frame with a matching `try`/`catch`. If no frame catches it, the job ends with the stack empty and the error is reported as uncaught. The runtime then continues with the next queued job, so one job's uncaught error does not stop later ones from running.
  • Does a nested call suspend the caller in the same sense that awaiting does?
    No. When `a()` calls `b()`, `a`'s frame stays on the stack underneath `b`'s and resumes the instant `b` returns — the job never ends and nothing else can run in between. Awaiting is different in kind: the function returns, its frame pops, and the remainder runs later as a separate job on an empty stack.

saying these in an interview costs you the question

  • JavaScript is multi-threaded because callbacks can run in parallel
  • setTimeout runs its callback on a background thread
  • The call stack is where queued callbacks wait their turn
  • Async functions keep executing while later code runs
  • The engine pauses a function to run an urgent callback

context

open as a page

In JavaScript, if a promise is already fulfilled at the moment you call .then(callback) on it, does callback run immediately? Explain when it actually runs and what schedules it.

level: juniorimportance: must knowfreq 66%

basics

~20 s

No. .then never calls back synchronously: it schedules a promise reaction job on the microtask queue. The callback runs after the currently executing script or task finishes, when the engine drains microtasks at its checkpoint.

open as a page

What does this script print, and why? console.log('start'); setTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => console.log('promise')); console.log('end');

level: juniorimportance: must knowfreq 85%

basics

~20 s

It prints start, end, promise, timeout. All the top-level synchronous code runs first, then the promise callback runs as a microtask once the script finishes, and the setTimeout callback runs last as a separate task.

open as a page

What does setInterval return, and what happens if the code that started a repeating timer is torn down without ever calling clearInterval with that value?

level: juniorimportance: must knowfreq 62%

basics

~20 s

setInterval returns a timer handle — a number in browsers, a Timeout object in Node — whose only use is to pass to clearInterval. Without that call the timer fires forever, keeping its callback and everything the callback closes over alive.

open as a page

Does setTimeout(fn, 100) guarantee that fn runs exactly 100 ms later? What does the delay argument actually promise?

level: juniorimportance: must knowfreq 78%

basics

~20 s

setTimeout's delay is a minimum wait, not an exact schedule. When it elapses the callback is only queued as a task; it runs after the currently executing code finishes and after any work already queued ahead of it, so it commonly fires late.

open as a page

In browser or Node JavaScript, what does setTimeout(fn, 0) actually do — when does fn run relative to the code written after the setTimeout call?

level: juniorimportance: must knowfreq 70%

basics

~20 s

setTimeout(fn, 0) schedules fn as a separate later task instead of running it now. Every remaining statement in the current script runs first, then all pending promise callbacks, and only then does fn execute — typically a few milliseconds later.

open as a page

In JavaScript, once a function starts running, can a pending timer callback or promise reaction interrupt it partway through? Explain what run-to-completion guarantees and where that guarantee stops.

level: middleimportance: must knowfreq 68%

basics

~20 s

No. A JavaScript job runs until its call stack is empty, and only then does the runtime pick up the next callback, so nothing is preempted between statements. The guarantee covers one uninterrupted synchronous run — an await ends it and starts a new job.

open as a page

What is the microtask checkpoint in a JavaScript runtime, and what exactly does the engine do with the microtask queue when it reaches one?

level: middleimportance: must knowfreq 72%

basics

~10 s

At a microtask checkpoint the engine drains the entire microtask queue, running jobs until the queue is empty — including microtasks enqueued during the drain — before it starts the next task.

open as a page

An async function in a browser tab runs `while (true) { await Promise.resolve(); }`. Does that await give the event loop a chance to run timers, dispatch input and repaint?

level: middleimportance: must knowfreq 55%

basics

~20 s

Awaiting an already-resolved promise does not yield to timers or input. The continuation after await is queued as a microtask, so the microtask queue never empties and the tab freezes just like a synchronous while(true) loop.

open as a page

What does this script print, and why? async function inner() { console.log('inner'); } async function outer() { console.log('outer start'); await inner(); console.log('outer end'); } console.log('script start'); outer(); Promise.resolve().then(() => console.log('then')); console.log('script end');

level: middleimportance: must knowfreq 75%

basics

~20 s

It prints script start, outer start, inner, script end, outer end, then. An async function runs synchronously up to its first await; everything after that await is a microtask, queued at the moment the await is reached.

open as a page

What does this script print, and why? console.log('A'); new Promise((resolve) => { console.log('B'); resolve(); console.log('C'); }).then(() => console.log('D')); console.log('E');

level: middleimportance: must knowfreq 70%

basics

~20 s

It prints A, B, C, E, D. The function passed to the Promise constructor runs synchronously, and calling resolve does not stop it or run the then callback — that callback is queued as a microtask and runs after the script finishes.

open as a page

In JavaScript, what is the difference between repeating work with setInterval(fn, 1000) and having fn schedule itself with setTimeout(fn, 1000) as its last step, and when does that difference matter?

level: middleimportance: must knowfreq 70%

basics

~20 s

setInterval measures its period from tick to tick, so a slow callback eats into the gap and can run back-to-back. A self-rescheduling setTimeout guarantees a full delay after each run finishes, at the cost of a period that grows by the callback's own duration.

open as a page

In a browser, a callback scheduled with queueMicrotask() calls queueMicrotask() on itself every time it runs. What happens to the page, and will a setTimeout callback that was already pending ever fire?

level: juniorimportance: should knowfreq 40%

basics

~20 s

The page freezes. The runtime drains the microtask queue until it is empty, and a microtask that re-enqueues itself never lets it empty, so the pending setTimeout callback, input handling and rendering never get a turn.

open as a page

In JavaScript, what makes an engine throw the error 'RangeError: Maximum call stack size exceeded', and why does rewriting the same recursive algorithm as a loop remove that limit?

level: middleimportance: should knowfreq 52%

basics

~20 s

Every call in progress needs a stack frame, and the engine's stack region is fixed in size; recursion that nests deeply enough exhausts it and V8 reports that as a RangeError. A loop reuses one frame, so nothing accumulates.

open as a page

In modern JavaScript, how does queueMicrotask(fn) differ from Promise.resolve().then(fn), given that both run fn at the next microtask checkpoint?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both run fn at the same microtask checkpoint, in enqueue order. queueMicrotask allocates no promise and returns nothing to chain, and a throw inside it surfaces as an uncaught error rather than becoming a promise rejection that can be silently swallowed.

open as a page

A routine that drains a large work queue schedules its next step with queueMicrotask(), and the browser tab is frozen for as long as it runs. How would you restructure it so the UI stays responsive?

level: middleimportance: should knowfreq 45%

basics

~20 s

Reschedule each step onto the task queue instead of the microtask queue — setTimeout(fn, 0), a MessageChannel message, or scheduler.yield() where available — and process a batch per turn so the runtime can run timers, input and rendering between batches.

open as a page

What does this script print, and why? setTimeout(() => { console.log('timer 1'); Promise.resolve().then(() => console.log('microtask in timer 1')); }, 0); setTimeout(() => console.log('timer 2'), 0);

level: middleimportance: should knowfreq 55%

basics

~10 s

It prints timer 1, microtask in timer 1, timer 2. A promise callback queued inside a timer callback runs as soon as that callback returns, before the runtime picks up the next timer callback.

open as a page

A status poller runs setInterval(() => refresh(), 1000), where refresh() is asynchronous and usually takes about three seconds. What goes wrong, and what would you use instead?

level: middleimportance: should knowfreq 48%

basics

~20 s

The timer ignores whatever refresh() returns, so it starts a new one every second regardless: about three run concurrently, and the count grows if refresh slows down. Replace the interval with a self-rescheduling setTimeout armed only after each run settles.

open as a page

In a browser, a function reschedules itself with setTimeout(step, 0) over and over. Why do the steps settle to roughly one every 4 ms instead of running back-to-back?

level: middleimportance: should knowfreq 45%

basics

~20 s

Browsers apply the HTML standard's nesting clamp: once a timer chain runs more than five levels deep, a requested delay below 4 ms is raised to 4 ms. The engine is not lagging — the host is enforcing a floor.

open as a page

Inside one task you call setTimeout(a, 100), then setTimeout(b, 100), then setTimeout(c, 50). In what order do the three callbacks run, and what rule decides it?

level: middleimportance: should knowfreq 35%

basics

~20 s

Order is decided by expiry time, with ties broken by registration order: c runs first because it expires 50 ms earlier, then a, then b. Registration order only matters between timers that come due at the same moment.

open as a page

Inside a long-running loop, does adding `await Promise.resolve()` on every iteration give the event loop a chance to run other queued work, the way `await new Promise(r => setTimeout(r, 0))` does?

level: middleimportance: should knowfreq 42%

basics

~20 s

No. Awaiting an already-resolved promise only queues a microtask, and microtasks are drained inside the current task before the runtime moves on, so the thread is never released. Only crossing a task boundary, such as awaiting a zero-delay timer, actually yields.

open as a page

You have to process a 200,000-item array in the browser and doing it in a single loop freezes the page. How do you use setTimeout to split that work so the page stays responsive, and what does the resulting code look like?

level: middleimportance: should knowfreq 50%

basics

~20 s

Keep a cursor into the array and process only as many items as fit in a small time budget, roughly 5 ms. When the budget is spent, call setTimeout with the continuation and a zero delay so the next batch runs as a fresh task, letting queued work run in between.

open as a page

In a browser page, a JavaScript click handler runs a four-second synchronous loop (or a JSON.parse of a very large string). Using the call stack, explain why the whole page stops responding until it finishes, and what happens to the clicks the user makes in the meantime.

level: seniorimportance: should knowfreq 56%

basics

~20 s

The handler holds the only call stack for four seconds, and no queued job can start until that stack is empty, so no other handler, timer or promise reaction runs and nothing repaints. Clicks made meanwhile are queued and their handlers fire in a burst afterwards.

open as a page

A script makes 500 DOM changes in one synchronous loop while a MutationObserver is watching. The observer's callback runs once with 500 records rather than 500 times. Why does it behave that way, and what does that imply for code written inside the callback?

level: seniorimportance: should knowfreq 28%

basics

~20 s

MutationObserver delivers records as a microtask, not synchronously per mutation. Mutations accumulate in the observer's record queue during the synchronous run and are handed to the callback in one batch at the microtask checkpoint, so the callback sees the final state.

open as a page

After a release, one page in a web app becomes unresponsive: the tab pins a CPU core, no error appears in the console, and log lines inside setTimeout callbacks never print. How would you establish that a microtask loop is starving the event loop, and distinguish it from a plain blocking synchronous loop?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Record a performance profile of the frozen tab. Microtask starvation shows thousands of short, repeating callbacks with a shallow stack inside one task, whereas a blocking loop shows a single long frame. Pausing the debugger lands in the self-rescheduling step.

open as a page

When you trace a JavaScript program that mixes synchronous logs, setTimeout callbacks and promise reactions, which parts of the resulting output order are guaranteed by the language, and which are incidental to the runtime and unsafe to depend on in production code or tests?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Guaranteed: current code runs to completion, all pending microtasks drain before the next task, and callbacks within one queue run first-in-first-out. Incidental: actual timer delays, ordering between unrelated task sources, and how many ticks a foreign promise implementation costs.

open as a page

A countdown decrements a seconds counter inside setInterval(tick, 1000) and renders it. On a busy page it is noticeably behind real time after an hour. Why does that happen, and how would you make it accurate?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Counting ticks measures how many times the timer fired, not how much time passed. Each tick can only be late, never early, and the lateness accumulates. Compute remaining time from a timestamp captured at the start rather than from a tick count.

open as a page

A browser page schedules work with setTimeout while the user switches to another tab for ten minutes. What does the browser do to those timers, and how do you write code that survives it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Hidden tabs get their timers throttled: typically no more than about once per second, and far less after several minutes in the background. Treat setTimeout as a wake-up hint and derive elapsed time from timestamps, never from how often a callback ran.

open as a page

You are designing a JavaScript library that coalesces many small updates into one flush. How do you decide between flushing at the microtask checkpoint (queueMicrotask or Promise.resolve().then) and flushing on a fresh task, and what does each choice cost?

level: principalimportance: should knowfreq 24%

basics

~20 s

Flush at the microtask checkpoint when the batch must settle before anything outside your library can observe it; flush on a task when the runtime needs control back first. Microtasks extend the current turn; tasks let unrelated work interleave.

open as a page

What happens when you call setTimeout(fn, 2 ** 31) — a delay of about 25 days — and why?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

The delay is held in a 32-bit signed integer, so anything above 2147483647 ms (about 24.8 days) overflows. Browsers then treat it as zero and fire almost immediately; Node clamps it to 1 ms and prints a TimeoutOverflowWarning.

open as a page

showing 1–30 of 31