---
title: 'The Single-Threaded Model'
source: 'https://academia.sh/en/courses/asynchronous-javascript/single-threaded-model'
course: 'Asynchronous JavaScript and the Runtime'
language: en
updated: '2026-08-17T18:09:43+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```js
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.

```js
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.
