skip to content

In a browser, `Atomics.wait()` throws a TypeError when called on the main thread, but the same call inside a dedicated Web Worker works. Why is that, and what do you use on the main thread instead?

level: seniorimportance: nice to knowfreq 24%

answer

  1. Wait parks the whole agent
  2. Main thread may not block
  3. Int32Array over shared memory only
  4. Notify is allowed anywhere
  5. Async variant returns a promise

basics

~20 s

Atomics.wait blocks the calling agent until notified. The browser's main-thread agent is not allowed to block — freezing it would freeze rendering and input — so the call throws TypeError there. Use Atomics.waitAsync, or have a worker do the waiting.

solid answer

~40 s

`Atomics.wait(int32Array, index, expected, timeout)` **blocks** the calling agent until another agent calls `Atomics.notify` on the same slot. Blocking is allowed only in agents flagged as able to block; a browser window agent is not, because a blocked main thread means no rendering, no input, no event loop at all. So the call throws a `TypeError` on the main thread while succeeding inside a dedicated worker. The typed array also has to be an `Int32Array` or `BigInt64Array` backed by a `SharedArrayBuffer`. On the main thread the options are `Atomics.waitAsync`, which returns `{ async, value }` where `value` may be a promise resolving to `"ok"`, `"not-equal"` or `"timed-out"`, or the usual design: let a worker block and report back with `postMessage`.

code

javascript · 16 lines
javascript
// Run on a cross-origin isolated page, from the main thread.
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);

try {
  Atomics.wait(i32, 0, 0, 0);
} catch (err) {
  console.log(err.name); // "TypeError" - the window agent cannot block
}

console.log(Atomics.notify(i32, 0)); // 0 - notify is allowed, nobody waiting

if (typeof Atomics.waitAsync === 'function') {
  const r = Atomics.waitAsync(i32, 0, 0, 50);
  Promise.resolve(r.value).then((s) => console.log(s)); // "timed-out"
}

go deeper

for a junior

Know that Atomics.wait blocks whichever thread calls it and that the browser forbids blocking the main thread, so the call is worker-only.

for a middle

Explain the agent-can-block rule, the Int32Array-over-SharedArrayBuffer requirement, and the three return values, and name Atomics.waitAsync as the non-blocking counterpart.

for a senior

Show the design consequence: put every blocking wait inside workers, let the page learn of completion through a message event, and reject spin loops as a main-thread workaround because they freeze rendering just as effectively.

for a principal

Decide whether shared-memory coordination is warranted at all against a message-passing or transfer-based design, given that it also drags in cross-origin isolation and a class of concurrency bugs that will not reproduce reliably.

## Agents and blocking Each JavaScript execution context in a browser — the page, each dedicated worker — is an **agent** with its own event loop. Agents in the same cluster can share memory through a `SharedArrayBuffer`, and the `Atomics` namespace provides the operations that make concurrent access to that memory well-defined: `Atomics.load`, `Atomics.store`, `Atomics.add`, `Atomics.compareExchange`, and the coordination pair `Atomics.wait` / `Atomics.notify`. `Atomics.wait` is different in kind from the others. It does not read or write a value and return; it **parks the whole agent**. The calling thread stops executing until another agent notifies the same memory slot, or the timeout expires. Nothing else runs on that agent in the meantime — not timers, not message handlers, not rendering. Every agent carries a flag for whether it is permitted to do that. Dedicated worker agents can block. The window agent that runs your page cannot, because blocking it would freeze the tab: no frames painted, no clicks dispatched, no messages delivered — including, fatally, the very `postMessage` or `Atomics.notify` that was supposed to wake it. Calling `Atomics.wait` there throws a `TypeError` rather than deadlocking the tab. ## The shape of the call ```js const sab = new SharedArrayBuffer(4); // requires cross-origin isolation const i32 = new Int32Array(sab); // in a worker: Atomics.wait(i32, 0, 0); // block while i32[0] is still 0 // in any agent, including the page: Atomics.store(i32, 0, 1); Atomics.notify(i32, 0); // wake waiters on slot 0 ``` Three details matter. The array must be an `Int32Array` or `BigInt64Array` over a `SharedArrayBuffer` — a non-shared buffer or a `Uint8Array` throws a `TypeError`. The `expected` argument is checked atomically before parking, which closes the race where the notifier fires between your read and your wait: if the value already differs, `wait` returns `"not-equal"` immediately. And the return value is a string, one of `"ok"`, `"not-equal"` or `"timed-out"` — always branch on it rather than assuming you were woken by a notify. `Atomics.notify(array, index, count)` is *not* restricted; it works fine on the main thread and returns how many waiters it actually woke, which is often zero because nobody was parked yet. ## What to do on the main thread **`Atomics.waitAsync`** is the non-blocking counterpart. It returns an object `{ async, value }`: when `async` is `false` the result was immediate and `value` is the string; when `async` is `true`, `value` is a promise that resolves to `"ok"` or `"timed-out"`. Because it yields to the event loop instead of parking it, the main thread stays responsive. Availability has been uneven across browsers, so feature-detect it (`typeof Atomics.waitAsync === 'function'`) and keep a fallback. **Message passing** is the design most codebases end up with anyway. The worker blocks on `Atomics.wait` if it needs to; the page never waits on memory at all, it just receives a `message` event when the work is done. That keeps the blocking on the side where blocking is legal and cheap. ## The wider point about shared memory in a UI The `TypeError` is not an inconvenience to work around — it is the platform enforcing the invariant the whole rendering model rests on: the main thread must return to its event loop. Any design where the page "waits for the worker to finish" is wrong on the browser, whether the waiting is `Atomics.wait`, a spin loop on `Atomics.load`, or a synchronous XHR. Spinning is arguably worse than the throw, because it burns a core, still blocks rendering, and does not even release the thread. So the honest use of `SharedArrayBuffer` in a page is: workers coordinate among themselves with atomics, possibly blocking each other; the main thread only ever *reads* results and *notifies*, and learns that work is finished through an ordinary event. If your design needs the page to block, the problem is the design, and moving the coordination point into a worker is the fix. All of this presumes the page is cross-origin isolated, since without that there is no `SharedArrayBuffer` for atomics to operate on in the first place.

  • What does the `expected` argument of Atomics.wait protect against?
    The lost-wakeup race. The comparison and the parking happen as one atomic step, so if another agent changed the slot between your read and your wait, the call returns `"not-equal"` immediately instead of parking forever. Without that check you could miss the notify and block indefinitely.
  • Would a spin loop on Atomics.load be an acceptable main-thread substitute?
    No. It burns a CPU core and still prevents the main thread from returning to its event loop, so rendering and input stay blocked — the same freeze the TypeError exists to prevent, minus the diagnostic. Wait for work asynchronously instead, via Atomics.waitAsync or a worker message.
  • Which agent should own the blocking in a worker-pool design?
    The workers. Let pool members park on Atomics.wait for work to appear and notify each other, while the page only stores values, calls Atomics.notify, and learns about completion through a message event. Blocking is legal and cheap on a worker agent and illegal on the window agent.

saying these in an interview costs you the question

  • Claiming Atomics.wait is simply unimplemented in browsers
  • Substituting a spin loop on the main thread
  • Passing a Uint8Array or a non-shared buffer to Atomics.wait
  • Ignoring the not-equal and timed-out return values
  • Assuming Atomics.notify is also main-thread forbidden

context