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?
answer
- Wait parks the whole agent
- Main thread may not block
- Int32Array over shared memory only
- Notify is allowed anywhere
- Async variant returns a promise
basics
~20 sAtomics.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// 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
Know that Atomics.wait blocks whichever thread calls it and that the browser forbids blocking the main thread, so the call is worker-only.
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.
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.
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