Concurrency#

JavaScript is single-threaded. One event loop drives everything; the Promise machinery and async / await are the operator’s only concurrency primitives by default. Real parallelism requires explicit workers (worker_threads, cluster, Worker in the browser); shared memory needs SharedArrayBuffer plus Atomics.

For async syntax, see Functions. For try / catch on promises, see Errors.

The event loop#

The runtime runs one task at a time off a queue, then drains a microtask queue to completion, then runs the next task.

  • Tasks (also called macrotasks): timers (setTimeout, setInterval), I/O callbacks, setImmediate (Node), UI events (browser).

  • Microtasks: Promise reactions (.then / .catch / .finally, await), queueMicrotask.

Microtasks always drain before the next task. That means an await inside an event handler resumes before the next setTimeout callback fires, even if the timeout was set to zero.

setTimeout(() => console.log("task"), 0);
queueMicrotask(() => console.log("microtask"));
console.log("sync");
// sync
// microtask
// task

Blocking the event loop blocks everything; the operator keeps synchronous work short (under a few ms per task) and pushes heavy work to a worker.

Promises#

A Promise represents a value that will be available later (or never). Three states: pending, fulfilled, rejected. Once settled, a promise’s state and value are immutable.

const p = fetch("https://example.com");
p.then(r => r.text()).then(console.log).catch(console.error);

async / await is the default form; the .then / .catch chain only makes sense at boundaries where async is not available.

async function fetchJson(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}

Composition#

Combinator

Behaviour

Promise.all([p1, p2, …])

Settles with an array of results when all fulfil; rejects fast on the first rejection.

Promise.allSettled([…])

Always resolves with an array of {status, value} / {status, reason}. Use when partial failure is OK.

Promise.race([…])

Settles with the first to settle (fulfil or reject).

Promise.any([…])

Resolves with the first to fulfil; rejects with AggregateError only if all reject.

// parallel fetches, fail-fast
const [a, b] = await Promise.all([fetch(u1), fetch(u2)]);

// parallel fetches, tolerate partial failure
const settled = await Promise.allSettled(urls.map(fetch));

// first responder
const winner = await Promise.race([primary(), backup()]);

Cancellation#

AbortController plus signal is the operator’s cancellation channel; fetch, setTimeout (Node 17+), readFile, and most stdlib I/O accept a signal.

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);    // 5-second budget

try {
  const r = await fetch(url, {signal: ctrl.signal});
} catch (e) {
  if (e.name === "AbortError") log("timed out");
  else throw e;
}

AbortSignal.timeout(ms) and AbortSignal.any([a, b]) are convenient when do not need the controller itself.

Workers (parallelism)#

For CPU-bound work the operator spawns a worker. Each worker is a new V8 isolate with its own event loop; communication is by postMessage.

// main.js
import {Worker} from "node:worker_threads";

const w = new Worker("./hash.js", {workerData: largeBuffer});
w.on("message", h => console.log("hash:", h));

// hash.js
import {parentPort, workerData} from "node:worker_threads";
import {createHash} from "node:crypto";

parentPort.postMessage(createHash("sha256").update(workerData).digest("hex"));

For multi-process scale-out, node:cluster forks the master process and shares a listening socket across workers. For serverless / container scale-out, use N processes behind a load balancer instead.

Shared memory#

SharedArrayBuffer plus Atomics exposes shared memory across workers. Atomics.wait and Atomics.notify cover the synchronisation primitives. The operator rarely needs this; a message-passing design is almost always clearer.

Browser concurrency#

In the browser pick from Web Worker (no DOM, own event loop), Service Worker (cache and fetch interception for offline), and Shared Worker (shared across tabs of the same origin).

const w = new Worker("worker.js");
w.postMessage({task: "hash", data});
w.onmessage = e => console.log(e.data);

References#