JavaScript runs one job at a time on a single thread per agent, with no preemption. Waiting is modelled with promises and jobs, while the event loop itself belongs to the host (ECMA-262, Promise Objects and Jobs and Host Operations to Enqueue Jobs).
Promises
A promise (ES2015) stands for a result that may not be ready yet.
| Term | Meaning |
|---|---|
| Pending | No result yet |
| Fulfilled | Finished with a value |
| Rejected | Finished with a reason |
| Settled | Fulfilled or rejected: it will never change |
| Resolved | Locked to an outcome, which may be following another promise |
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve(42), 100);
});
p.then(v => v + 1) // a new promise for 43
.then(v => { throw new Error('bad'); })
.catch(e => 0) // recovers, giving 0
.finally(() => stop()); // runs either way; the value passes through
- The executor runs synchronously, and a throw in it rejects the promise.
- Only the first of
resolveandrejectcounts. - Resolving with a promise, or any thenable (an object with a
thenmethod), makes the outer promise adopt its outcome. thencallbacks always run later as jobs, even when the promise is already settled.thenandcatchreturn a new promise. What the callback returns or throws decides its outcome.
Promise statics
| Method | Result | Since |
|---|---|---|
Promise.resolve(v) | A promise resolved with v | ES2015 |
Promise.reject(e) | A rejected promise | ES2015 |
Promise.all(xs) | Array of all values, or rejects with the first failure | ES2015 |
Promise.race(xs) | The first to settle | ES2015 |
Promise.allSettled(xs) | Array of { status, value } or { status, reason } | ES2020 |
Promise.any(xs) | The first to fulfil, or rejects with an AggregateError | ES2021 |
Promise.withResolvers() | { promise, resolve, reject } | ES2024 |
Promise.try(fn, ...args) | Calls fn now and wraps the result or throw in a promise | ES2025 |
Each combinator takes any iterable and calls Promise.resolve on the
items.
Async functions and await
AsyncFunctionDeclaration:
async [no LineTerminator here] function BindingIdentifier ( FormalParameters ) { AsyncFunctionBody }
AsyncArrowFunction:
async [no LineTerminator here] AsyncArrowBindingIdentifier [no LineTerminator here] => AsyncConciseBody
AsyncMethod:
async [no LineTerminator here] ClassElementName ( UniqueFormalParameters ) { AsyncFunctionBody }
AwaitExpression:
await UnaryExpression
async function load(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(res.status);
return await res.json();
} catch (e) {
return null;
}
}
- An async function (ES2017) always returns a promise. A returned value fulfils it, and a throw rejects it.
- Its body runs synchronously until the first
await. await xpauses the function and resumes it as a job whenxsettles, giving the value or throwing the reason. A non-promise value resumes on the next job.awaitis allowed in async functions, async generators, and the top level of modules. Elsewhere it is an identifier in scripts, or an error in modules.awaitis not allowed in parameter defaults of an async function.- Run tasks in parallel with
Promise.all, because sequentialawaits wait in turn.
Jobs and the event loop
ECMA-262 defines jobs, units of work a host runs when nothing else is
running. Promises schedule theirs through HostEnqueuePromiseJob, with
the guarantee that they run in FIFO order, and only when no other job is on the
stack. Timers, I/O, rendering and the event loop are defined by the host: the HTML
standard for browsers, and libuv and Node.js for Node.js.
In browsers these reaction jobs are called microtasks. After each task,
and whenever the stack empties, the microtask queue is drained completely before
anything else runs. queueMicrotask adds to the same queue.
console.log('a');
setTimeout(() => console.log('d'));
Promise.resolve().then(() => console.log('c'));
console.log('b');
// prints a, b, c, d
async function f() {
console.log(1);
await null;
console.log(3);
}
f();
console.log(2);
// prints 1, 2, 3
- A long run of microtasks can starve rendering and timers, since the queue is drained first.
- Each worker is a separate agent with its own event loop and memory. They
communicate by messages or, with
SharedArrayBufferandAtomics, through shared memory.
Unhandled rejections
A rejected promise with no handler is reported through
HostPromiseRejectionTracker. Browsers fire an
unhandledrejection event. Node.js by default raises it as an
uncaught exception and exits.
Shared memory and Atomics
SharedArrayBuffer and Atomics (ES2017) let agents share bytes
and synchronise. Atomics offers atomic reads, writes and
compare-and-swap, with wait, notify and
waitAsync (ES2024) for waiting. The rules for data races are the Memory
Model clause of ECMA-262. Browsers expose SharedArrayBuffer only to
cross-origin isolated pages.