Skip to content
academia.sh

Lesson 01 / 17

The Single-Threaded Model

JavaScript's single-threaded, run-to-completion execution model; the consequences of blocking work; the boundary between the language's core and the asynchronous abilities the runtime provides.

Contents

The previous two courses built the language’s value model: values, objects, the prototype chain, and closures. All of it was explained under a single assumption — code runs in the order it is written, one statement ends, the next begins. This course lifts that assumption.

When you ask a measurement station for data, the response does not arrive right away. What does the program do while the response is awaited? The answer “it waits” costs a great deal in a single-threaded language. This lesson establishes why the decision to wait cannot be made, and what the language does instead.

What a Single Thread Means

A thread is a single flow of execution that runs commands in order. A JavaScript program has only one of these flows: at any given moment, a single line of user code runs. Two functions never advance at the same time, two loops never interleave, two callbacks never collide.

This has a direct consequence: there is no data race. No other code can slip in between two lines where you read and write a variable. A counter increment that would need a lock in other languages needs none here.

The operating system’s process and thread concepts, kernel-level scheduling, and real parallelism are a separate subject, covered in the Operating System Concepts course. This course focuses on the code that falls to a single thread.

Run-to-Completion

The model’s second rule is as binding as the single thread: once a piece of work starts running, it is not broken up until it finishes on its own. This is called run-to-completion.

This means that even a piece of work registered as “run after zero milliseconds” waits for the work ahead of it to finish.

function computeSum(rounds) {
  let total = 0;
  for (let i = 0; i < rounds; i += 1) total += i % 7;
  return total;
}

setTimeout(() => console.log("3 — timer callback"), 0);

console.log("1 — computation starts");
const result = computeSum(5_000_000);
console.log("2 — computation done, result:", result);
1 — computation starts
2 — computation done, result: 14999995
3 — timer callback

The callback was registered first, with a delay of zero; yet it ran last. setTimeout does not run a piece of work now, it queues it. Queued work is only taken up once the currently running code finishes. This principle is opened up in detail in the next lesson.

Run-to-completion is the natural companion of a single thread. If you assumed another callback could run in the middle of a running function, you would be back to a discussion of locks and reentrancy. The model chooses simplicity to keep this complexity away from the person writing the program — its cost is that every piece of work in the queue waits for the one ahead of it.

Blocking Work

Blocking work is work that keeps the thread busy in a way that lets it do nothing else. It has two sources.

The first is a long-running computation: sorting an array with millions of elements, parsing a large piece of text. These actually do the work, they just take a long time.

The second, and more insidious, is synchronous I/O calls. A call that reads a file synchronously holds the thread until the disk responds; yet during this time the thread does no computation at all, it just waits.

import { writeFileSync, readFileSync, readFile } from "node:fs";

writeFileSync("measurements.txt", "A1;21.4\nB2;19.8\nC3;23.1\n");

readFile("measurements.txt", "utf8", (error, data) => {
  console.log("3 — asynchronous read done:", data.trim().split("\n").length, "lines");
});

const content = readFileSync("measurements.txt", "utf8");
console.log("1 — synchronous read done:", content.trim().split("\n").length, "lines");
console.log("2 — rest of the script");
1 — synchronous read done: 3 lines
2 — rest of the script
3 — asynchronous read done: 3 lines

Both reads read the same file and produce the same result. Where they part is who owns the thread while waiting. The synchronous one holds it; the asynchronous one releases it and reports the result with a callback once it is ready.

Blocking’s visible consequence varies by environment. In a browser, the interface freezes: clicks are not handled, drawing does not update, scrolling sticks. On a server, a single request’s long computation makes every request arriving in the meantime wait in a queue. In both cases the cause is the same: the thread is one, and the work in the queue is patient.

Where the Language Ends and the Runtime Begins

The language’s core does not define asynchrony. The standard defines promises and async functions; but none of the concepts like “after ten milliseconds” or “once the file is read” appear anywhere in the language’s syntax.

These abilities are supplied by the host environment. The distinction introduced in the JavaScript Fundamentals course becomes functional here:

Layer What it supplies
Language core Values, functions, promises, async/await, the microtask rule
Host environment Timers, network requests, file access, events, the task queue, the event loop

setTimeout is not a language keyword; it is a function the host environment makes available. The function that starts a network request belongs to the host environment in the same way. This is why the same JavaScript code meets different abilities in different environments: a browser has a document object, a server runtime has a file system.

The distinction also splits responsibility. Waiting is not done by the language. The host environment sets up the timer and, once the duration elapses, places the callback in the queue; once the thread is free, JavaScript runs that callback. While waiting, the thread is free.

Asynchrony Is Not Parallelism

This distinction holds throughout the course and needs to be settled from the start.

Parallelism is two pieces of work running at the same time; it requires more than one thread. Asynchrony is a piece of work being started and its result picked up later; it is possible with a single thread too.

When you start three measurement requests “at the same time,” the waiting time these requests spend on the network really does overlap — because the waiting is done by the host environment, not JavaScript. But the code that processes the responses runs in order: while the first response is being processed, the second waits in the queue. So your program is parallel in waiting, sequential in computation.

The practical consequence is this: asynchrony speeds up I/O-heavy work; it does not speed up computation-heavy work. Putting a million-element sort inside setTimeout does not speed it up, it only delays its start. To actually spread out a computational load, a separate thread is needed; runtimes offer this as a separate execution context, and only messages are passed between contexts — there is no shared variable.

The Course’s Example: A Measurement Stream

Throughout the course, a single concrete example will be worked through: a small program collecting temperature values from a few measurement stations. The stations’ response delays differ; some return an error, some never respond at all.

This example will go through the following steps, lesson by lesson: a single measurement with a callback, a measurement chained with a promise, a batch of measurements with combinators, a measurement bounded by a timeout, a cancellable measurement, and finally a continuous measurement stream that leaves no leak.

Nothing printed throughout the program will ever be a duration; only order will be printed. Duration varies by machine and load; order is the model’s rule.

Summary

  • JavaScript user code runs on a single thread; two pieces of code never advance at the same time, so no data race occurs.
  • Under the run-to-completion rule, the work in the queue is not taken up until the running work finishes; even a zero-delay callback waits for this.
  • Blocking work keeps the thread busy and delays every waiting piece of work; a long computation and synchronous I/O are its two sources.
  • Timers, network, and file access are abilities of the host environment, not the language; the language defines only promises and the microtask rule.
  • Asynchrony overlaps waiting, it does not parallelize computation.

Next Step

This lesson said callbacks “get in queue” but did not say how the queue works. It was not shown that not every piece of work in the queue is equal, or that a promise callback runs before a timer callback registered earlier. The next lesson opens up this mechanism: it establishes the event loop’s full rule through the distinction between the call stack, the task queue, and the microtask queue, and justifies a mixed example’s output order line by line.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close