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:
Promisereactions (.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 |
|---|---|
|
Settles with an array of results when all fulfil; rejects fast on the first rejection. |
|
Always resolves with an array of |
|
Settles with the first to settle (fulfil or reject). |
|
Resolves with the first to fulfil; rejects with
|
// 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.
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#
Functions for the
async/awaitsyntax.Errors for
try/catchand promise rejection.Networking for I/O that drives the event loop.
Runtime for the Node / Deno / Bun loop differences.