Photo by Yosuke Ota on Unsplash

On this page

This series follows ECMAScript versions cheat sheet. That page lists what the language contains. This series is about what happens when a program written in it runs: who turns the source into something a CPU can execute, which thread runs it, when a callback gets its turn, and when memory is given back. This first page is the overview; each section ends at the chapter that goes deeper.

What “single-threaded” means

The usual summary is that JavaScript is single-threaded. That is accurate in a narrow sense. Each place JavaScript runs — the specification calls it an agent — has one call stack and executes one piece of your code at a time. A function you call runs until it returns, and no other code of yours interleaves with it on that thread.

The program around your code is not single-threaded. While your function runs, the browser downloads files on network threads, the engine compiles hot functions and marks garbage on helper threads, and Node hands file reads and DNS lookups to a pool of native threads. What is single is the thread that runs your JavaScript, not the process.

Two consequences follow from that:

  • Asynchronous is not parallel. fetch does not run your callback on another thread. The network work happens elsewhere; your callback runs later, on the same thread, once that thread is free.
  • Long synchronous work blocks everything behind it. Other callbacks, timers, and — in a browser — rendering and input handling all wait until the current function returns.

When you need JavaScript itself to run in parallel, you start another agent: a worker, with its own stack and heap.

Engine, runtime, and host

Three words get used interchangeably, and they mean different things.

The engine implements ECMA-262, the language specification. It parses and compiles source, executes it, and owns the heap and the garbage collector. The engines in wide use are V8 (Chrome, Edge, Node, Deno), SpiderMonkey (Firefox), and JavaScriptCore (Safari, Bun). An engine knows Array, Promise, and JSON. It does not know what a file, a button, or an HTTP request is.

The host embeds the engine and supplies everything else: the DOM, fetch, setTimeout, console, fs, process. The host also owns the event loop. The engine can run a job when asked, but the host decides which job comes next. Browsers follow the event loop in the HTML Standard; Node builds its loop on the libuv library.

The runtime is the combination you actually program against. “Node is a JavaScript runtime” means V8, plus libuv, plus Node’s own APIs.

Diagram 1
flowchart TB
  subgraph engine["Engine"]
    stack["Call stack"]
    heap["Heap and garbage collector"]
  end
  subgraph host["Host"]
    apis["Host APIs: DOM, fetch, timers, fs"]
    native["Native threads: network, disk"]
    loop["Event loop and task queues"]
  end
  stack -->|"calls"| apis
  apis -->|"starts work"| native
  native -->|"queues a task when done"| loop
  loop -->|"runs the next callback"| stack

The split answers the recurring question of whether something “is JavaScript”. Promise is: ECMA-262 defines it, so every engine has it. setTimeout is not: the HTML Standard defines it, and Node ships its own version that returns a Timeout object instead of a number. Code that uses only ECMA-262 runs in any host. Code that touches host APIs runs where those APIs exist.

One program, start to finish

Here is one browser script, traced through every layer:

index.html
<button id="save">Save</button>
<script type="module">
const button = document.querySelector('#save');
button.addEventListener('click', async () => {
const response = await fetch('/api/save', { method: 'POST' });
button.textContent = response.ok ? 'Saved' : 'Failed';
});
</script>
  1. Parse and compile. The browser hands the source to the engine. The engine parses all of it — a syntax error anywhere stops the script before any line runs — and compiles the top-level code to bytecode. The click handler is only checked, not compiled; that happens the first time it is called.
  2. Run the script. Running the script is one task. The top-level code executes on the call stack. querySelector and addEventListener are host APIs: the engine calls into the host, which records the handler. Then the stack empties and the task ends. Nothing of yours is running; the page is idle.
  3. Wait for input. The user clicks. The browser works out, outside JavaScript, that the click landed on the button, and queues a task to dispatch the event.
  4. Run the handler. The event loop takes that task and calls the handler on an empty stack. fetch asks the host to start the request on a network thread and returns a pending promise. At await, the async function suspends: its state is saved off the stack, the stack empties, and the task ends. During the request, the page keeps rendering and responding to other input.
  5. Resume. The response arrives. The host queues a task that resolves the promise, and resolving it queues a microtask that resumes the handler. The handler runs to its end and sets textContent.
  6. Render. After the task and its microtasks, the browser updates the rendering: style, layout, paint. Only now does the button read “Saved”.
  7. Collect. The response object and the handler’s suspended state are no longer reachable. A later garbage collection reclaims their memory.

Every chapter of the series is one of those steps taken apart.

The engine: compilation and objects

Engines neither interpret the source line by line nor compile it once ahead of time. They parse it into a tree, compile that to bytecode an interpreter can start running immediately, and later compile the functions that run often into machine code, specialized for the types they observed while interpreting. That last step is just-in-time (JIT) compilation. It is speculative: optimized code assumes the types it saw will keep arriving. When they stop, the engine discards that code and returns to bytecode — a deoptimization.

Much of that speculation is about the layout of objects. In the language, an object is a dynamic collection of named properties. Inside the engine, objects created with the same properties in the same order share a hidden class, so point.x can compile to a load from a fixed offset. Code that keeps object layouts stable keeps that path fast.

From source code to machine code covers parsing, bytecode, the compiler tiers, and deoptimization. Hidden classes and inline caches covers object shapes, array element kinds, and how each property access specializes.

The runtime: stack, queues, and memory

Each function call pushes a frame onto the call stack, linked to the environment that holds the function’s variables. A return pops it. The stack is why a stack trace can show how execution got somewhere, and why runaway recursion ends in a RangeError instead of running forever. See Execution contexts and the call stack.

The event loop decides what runs when the stack is empty. Hosts keep two kinds of queue. Tasks are the large units — running a script, a timer callback, dispatching an event — and the loop runs one at a time. Microtasks, mostly promise reactions, run after each task until none are left. Browsers render between tasks, which is why one long task freezes the page. The event loop, tasks, and microtasks explains the order; Breaking up long tasks is the fix for a page that stops responding.

Objects, arrays, closures, and strings live on the heap. You never free memory yourself. The garbage collector reclaims objects that can no longer be reached from the stack, from globals, or from references the host holds. A memory leak in JavaScript is therefore never a forgotten free(); it is a reference you did not mean to keep. Memory management and garbage collection explains how the collector finds garbage, and Finding memory leaks is the procedure for finding the unwanted reference.

Beyond the main thread

A worker is another agent: its own thread, call stack, heap, and event loop. Browsers offer dedicated workers, shared workers, and service workers; Node offers worker_threads. Workers do not share variables with the page. They exchange messages, which are copied with the structured clone algorithm or transferred outright. Real shared memory exists through SharedArrayBuffer and Atomics, and browsers allow it only on cross-origin isolated pages.

Workers help with CPU-bound work. They do not speed up I/O-bound code, which already waits off the main thread. See Concurrency with workers.

Reading order

Chapter Answers
From source code to machine code How source becomes machine code
Hidden classes and inline caches Why object layout affects speed
Execution contexts and the call stack Where variables live
The event loop, tasks, and microtasks What runs in which order
Breaking up long tasks Fixing a page that stops responding
Memory management and garbage collection How garbage is found
Finding memory leaks Which reference keeps memory alive
Concurrency with workers Running JavaScript in parallel
JavaScript engine glossary Every term, defined briefly

Next: From source code to machine code.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. Which statements about JavaScript being single-threaded are accurate?Select all that apply.

Search articles

Type a word to search published articles.

Open the search page