cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

The memory model

Learn how JavaScript shared memory handles data races, sequential consistency, spin locks, and lock-free compare-and-swap retries.

By the end, you can
  • 01
    Recognize a data raceSeparate an ordinary shared read-modify-write from one atomic operation and explain why timing cannot be trusted.
  • 02
    Use atomic orderingExplain the single order seen by atomic operations and publish data with an atomic ready flag.
  • 03
    Choose a coordination patternCompare a short spin lock, waiting, and a lock-free compare-and-swap retry for a small shared-state problem.

What the memory model promises

A worker has its own JavaScript variables, stack, and event loop. A SharedArrayBuffer is the unusual part: two agents can access the same bytes. The memory model says which shared-memory results JavaScript allows when agents read and write those bytes.

An agent is one JavaScript execution participant, such as a worker. A memory event is a shared read, shared write, or atomic read-modify-write. You do not need to draw the specification's event graphs every day. You do need to know when a shared update is ordinary and when it must be atomic.

Definition

JavaScript's memory model defines the allowed observations of shared memory. Atomic operations are sequentially consistent, while a program with a data race can observe surprising but specified values instead of having undefined behavior.

This lesson continues SharedArrayBuffer & Atomics. That lesson showed the API calls. Here we explain the ordering promise behind them. Coming next, Parallel patterns with workers puts small coordination rules into worker-pool designs.

Data races

A data race occurs when agents make conflicting shared accesses and the accesses are not all atomic. The usual danger is one agent writing while another reads or writes the same location. A source line such as cells[0] += 1 looks small, but its idea has three steps: read the old value, add one, and write the new value.

Two ordinary reads can see the same balancePop out in the code editor (opens in a new tab)JavaScript
let balance = 100;const a = balance;const b = balance;balance = a + 50;balance = b + 50;console.log(balance);

Line 1 starts the balance at 100. Lines 2 and 3 model two agents reading 100 before either writes. Line 4 writes A's deposit and line 5 writes B's deposit from B's old read. Line 6 prints 150. This is a teaching schedule chosen to make the lost update visible.

Do not turn that model into a claim that every race loses one deposit. A real schedule can happen differently, and the specification permits more surprising observations around racy shared accesses. The important rule is simpler: do not depend on a result from a data race.

Real-life analogyTwo ATMs update one balance

Imagine two people depositing money at two ATMs at the same time. Both screens read 100, both add 50, and both write 150. One deposit disappears because neither ATM reserved the balance change as one action.

In real life: Both screens show 100
In JavaScript: Both agents read one shared cell
In real life: Each adds a 50 deposit
In JavaScript: Each computes from its local read
In real life: One screen overwrites the other
In JavaScript: A plain write can lose an update
In real life: The bank allows one change at a time
In JavaScript: An atomic update has one order

Where the analogy stops: A bank has business rules and records. The analogy only explains a conflicting read and write.

A lost update model

The next replay uses the same small balance program. It records one honest sequence of ordinary reads and writes. The replay is not an engine debugger and it does not predict when your workers will run; it makes one bad interleaving easy to inspect.

Step through one lost-update schedule
Step 0 of 6Ready
Your turn: follow the blue line

Replay an instrumented teaching model of one possible lost update. It is not a debugger attached to two workers.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const a = balance;const b = balance;balance = a + 50;balance = b + 50;console.log(balance);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Start with the two reads. Both retained local values are 100. After A writes 150, B still has the earlier value in its own local variable. B's later write therefore replaces 150 with another 150. The missing deposit happened between the separate read and write, not because JavaScript forgot how addition works.

Playground: choose an interleaving
The interleaving modelJavaScript
let balance = 100;let a;let b;// Choose each step below in the playground.a = balance;b = balance;balance = a + 50;balance = b + 50;console.log(balance);
Chosen schedulebalance 150
A reads
B reads
A writes
B writes
eventsA read 100 | B read 100 | A wrote 150 | B wrote 150

Both modeled agents read and wrote.

Step 4 of 44 of 4 chosen steps

Plain model: A read 100; B read 100; A wrote 150; B wrote 150. Final balance: 150. This is a chosen schedule, not a prediction of every race.

This is a deterministic teaching model. Real workers may run in another order, so a plain shared update must never be relied on.

Choose the steps in a different order. If A completes its read and write before B reads, the plain model reaches 200. Switch to atomic deposits and each write-shaped step becomes one Atomics.add, so two deposits always reach 200 in this model.

Sequential consistency

Sequential consistency means atomic shared-memory operations appear in one strict total order that every agent agrees on. Each agent keeps its own program order, and together the atomic events have one shared ordering. It is a strong, useful mental model: imagine everyone reading the same single register of atomic actions.

Two atomic deposits are one-at-a-time updatesPop out in the code editor (opens in a new tab)JavaScript
const cell = new Int32Array(new ArrayBuffer(4));Atomics.store(cell, 0, 100);Atomics.add(cell, 0, 50);Atomics.add(cell, 0, 50);console.log(Atomics.load(cell, 0));

Line 1 creates one integer cell. Line 2 stores 100. Lines 3 and 4 each atomically add 50, with no other atomic operation splitting either add. Line 5 loads and prints 200. This tiny runnable example uses an ordinary ArrayBuffer so it works in a browser sandbox; the ordering idea is the same when workers use an integer view over shared bytes.

Atomic ordering is not a magic label for an entire application. Keep a clear layout: use a cell as a flag, counter, or lock word, and do not mix ordinary and atomic accesses to the same contested location. The specification recommends data-race-free programs because then the whole program has intuitive interleaving behavior.

Shared operations that are easy to confuse
ThingWhat it isOrdering storyUse it for
Plain shared accessAn ordinary typed-array read or writeNo one total order across agentsCan participate in a data race
Atomic accessAn Atomics operation on an integer cellOne strict total order for atomic eventsUse for contested flags and counters
A lockA shared atomic state that grants one ownerThe lock operations are atomicProtect a short non-atomic critical section
Lock-free retryCompare-and-swap until the state matchesEach compare-and-swap is atomicAvoids holding a lock, but may retry

Publishing a message

A common pattern uses one ordinary data cell and one atomic flag. The writer fills the data first, then atomically says it is ready. The reader atomically checks the flag, then reads the data only after it observed ready. The flag is the small shared handshake.

Write data, then publish a ready flagPop out in the code editor (opens in a new tab)JavaScript
const cells = new Int32Array(new ArrayBuffer(8));const data = 0;const ready = 1;cells[data] = 42;Atomics.store(cells, ready, 1);if (Atomics.load(cells, ready) === 1) {  console.log(cells[data]);}

Lines 1 through 3 create two cells and name their jobs. Line 4 puts 42 in the data cell. Line 5 atomically stores ready as 1. Line 6 atomically reads the flag; only when it sees 1 does line 7 print 42. The flag matters because it gives both agents a known atomic observation point.

This is a small teaching shape, not a complete mailbox. A real worker protocol needs ownership, whether data can be replaced, and a plan for waiting or notification. Use postMessage for rich job details; use shared cells when a small shared state is genuinely useful.

Atomics.pause and spin locks

A spin lock repeatedly tries to acquire a lock instead of sleeping. It can make sense for a very short wait in a worker, but spinning burns CPU while it fails. A lock word usually uses 0 for unlocked and 1 for locked, with compare-and-swap acquiring it.

A feature-detected pause hint and tiny lockPop out in the code editor (opens in a new tab)JavaScript
function pauseHint() {  if (typeof Atomics.pause === "function") Atomics.pause();} function tryLock(lock) {  return Atomics.compareExchange(lock, 0, 0, 1) === 0;} function unlock(lock) {  Atomics.store(lock, 0, 0);} const lock = new Int32Array(new ArrayBuffer(4));console.log(tryLock(lock));unlock(lock);console.log(Atomics.load(lock, 0));

Lines 1 and 2 call Atomics.pause only when it exists. Lines 5 through 7 attempt to replace unlocked 0 with locked 1. Lines 9 through 11 release the lock by storing 0. Lines 14 through 17 show the tiny model printing true then 0.

Atomics.pause has no observable return value. It is a CPU hint that the thread is spinning and may do nothing on a platform. MDN lists it in Node 24 and newer browser versions, not Node 22, so feature detection is required. Prefer Atomics.wait for a long wait in an allowed worker, and never spin on the browser main thread because it freezes the page.

Short wait only

First try a short bounded spin only when you have measured low contention. If the lock is still held, wait or redesign the work. A loop that spins forever can prevent the lock holder from getting enough CPU time to release it.

Lock-free retry loops

Lock-free means an algorithm changes shared state with atomic operations and retry loops instead of holding a lock. One agent may need to retry, but the system can still make progress when some compare-and-swap succeeds. This is a design property, not a prize to chase for every counter.

Read, compare, and update one counterPop out in the code editor (opens in a new tab)JavaScript
const cell = new Int32Array(new ArrayBuffer(4));Atomics.store(cell, 0, 3);const observed = Atomics.load(cell, 0);Atomics.compareExchange(cell, 0, observed, observed + 1);console.log(Atomics.load(cell, 0));

Line 2 stores 3. Line 3 remembers that value. Line 4 asks to replace 3 with 4 only if the cell is still 3. With no competing change, line 5 prints 4. When another agent changes the value first, compare-and-swap returns the new old value and the algorithm reads, recalculates, and retries.

Step through a compare-and-swap retry
Step 0 of 6Ready
Your turn: follow the blue line

Replay an instrumented compare-and-swap retry. The model makes the competing update explicit so the retry is easy to see.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const cell = new Int32Array(new ArrayBuffer(4));const observed = Atomics.load(cell, 0);Atomics.compareExchange(cell, 0, observed, observed + 1);console.log(Atomics.load(cell, 0));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay inserts a competing atomic write between the first read and first compare-and-swap. The stale attempt returns 4 and changes nothing. The retry reads 4, swaps in 5, and completes. Real lock-free stacks add hazards such as allocation, removed nodes, and version changes, so a plain atomic counter is the right first example.

Two workers, exact totals

Teaching models explain one schedule. Node's worker_threads lets the lesson test demonstrate two stable runtime facts. Two workers each run one thousand Atomics.add operations on a SharedArrayBuffer counter, and the final total is exactly 2000.

Node-only atomic worker counterJavaScript
const { Worker, isMainThread, workerData, parentPort } = require("node:worker_threads");if (isMainThread) {  const buffer = new SharedArrayBuffer(4);  const workers = [0, 1].map(() => new Worker(__filename, { workerData: buffer }));  Promise.all(workers.map((worker) => new Promise((done) => worker.once("exit", done))))    .then(() => console.log(new Int32Array(buffer)[0]));} else {  const cell = new Int32Array(workerData);  for (let index = 0; index < 1000; index += 1) Atomics.add(cell, 0, 1);  parentPort.close();}

The main thread creates shared bytes on line 3 and waits for both workers to exit. Each worker runs the atomic increment on line 9 one thousand times. The child-process test writes the program to a temporary file and verifies that it prints 2000.

Node-only spin lock protects one plain counterJavaScript
const { Worker, isMainThread, workerData, parentPort } = require("node:worker_threads");if (isMainThread) {  const buffer = new SharedArrayBuffer(8);  const workers = [0, 1].map(() => new Worker(__filename, { workerData: buffer }));  Promise.all(workers.map((worker) => new Promise((done) => worker.once("exit", done))))    .then(() => console.log(new Int32Array(buffer)[1]));} else {  const cells = new Int32Array(workerData);  for (let index = 0; index < 1000; index += 1) {    while (Atomics.compareExchange(cells, 0, 0, 1) !== 0) {}    cells[1] += 1;    Atomics.store(cells, 0, 0);  }  parentPort.close();}

This second program reserves cell 0 for the atomic lock and cell 1 for the ordinary counter. Line 11 acquires the lock, line 12 performs the non-atomic increment while it is protected, and line 13 releases it. The two workers again print exactly 2000.

Neither probe tries to prove a non-atomic race will lose updates. That would be unstable evidence. The lesson uses the deterministic model to teach the risk and real workers only to prove the reliable atomic and lock outcomes.

Choose the smallest coordination tool

Start with messages. A worker pool can receive jobs with postMessage and send a result back the same way. Add shared memory only for a small hot state such as a completed-job counter, a ready flag, or a compact ring-buffer index that you have designed and tested.

Name each cell's purpose in code or documentation. Keep atomic flags and ordinary data in separate cells. Decide who writes each field, when a reader may inspect it, and how a waiting worker wakes. These plain questions prevent most shared-memory bugs before performance tuning starts.

Sort the shared-memory tools
  • cells[0] reads a shared value
  • cells[0] += 1 across workers
  • Atomics.add(cells, 0, 1)
  • Atomically store a ready flag
  • Retry compareExchange to acquire a lock
  • Retry after compare-and-swap returns another value
Try it yourself
0 of 6 correct

Place each card with the kind of operation or coordination pattern it describes.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “A race always prints the wrong number.” It can look correct in one run and still be unreliable.
  • “Data races mean JavaScript can crash with undefined behavior.” ECMAScript specifies allowed executions, even though values can surprise you.
  • “Atomic means every related object is protected.” An atomic operation covers its integer cell; the surrounding protocol is your design.
  • “pause is a sleep.” It is only a spinning hint and may have no observable effect.
  • “Lock-free means no retries.” A compare-and-swap algorithm may retry whenever another agent wins first.
Terms that sound similar but differ
TermWhat it meansDo not confuse it with
Data raceConflicting shared accesses without the needed atomic orderingA JavaScript crash or undefined behavior
Sequential consistencyA single shared order for atomic operationsA promise that ordinary reads never need design
Atomics.pauseA feature-detected CPU spin hintA sleep, yield, or correctness mechanism
Lock-freeAn algorithm that retries atomic changes without a lockAlways faster or automatically simpler

The specification's practical advice is conservative: keep programs data-race free. Use atomic operations consistently for contested coordination and do not access the same location with mismatched atomic and non-atomic rules.

Practice exercises

Exercise 1 · Warm-upPredict the chosen lost update

Read the teaching model. What number does it print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
let balance = 100;
const a = balance;
const b = balance;
balance = a + 50;
balance = b + 50;
console.log(balance);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict atomic deposits

    What is the final value after the two atomic deposits?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cell = new Int32Array(new ArrayBuffer(4));
    Atomics.store(cell, 0, 100);
    Atomics.add(cell, 0, 50);
    Atomics.add(cell, 0, 50);
    console.log(Atomics.load(cell, 0));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName the publishing handshake

      Which cell must the reader observe atomically before it reads the data value?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const cells = new Int32Array(new ArrayBuffer(8));
      const data = 0;
      const ready = 1;
      cells[data] = 42;
      Atomics.store(cells, ready, 1);
      if (Atomics.load(cells, ready) === 1) {
        console.log(cells[data]);
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeChoose a long-wait tool

        Your worker may wait many milliseconds for a queue item. Which tool is better than an unbounded spin loop?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRead a successful compare-and-swap

          What final number does this no-contention compare-and-swap example print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const cell = new Int32Array(new ArrayBuffer(4));
          Atomics.store(cell, 0, 3);
          const observed = Atomics.load(cell, 0);
          Atomics.compareExchange(cell, 0, observed, observed + 1);
          console.log(Atomics.load(cell, 0));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply this to a worker pool

            A photo-processing site needs workers to receive file details, options, and errors. What should it use for those complex job details?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Trace one source line into its actual read, write, or atomic action. Then ask which agent can observe which shared state.

              Memory model quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which situation is a data-race risk?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this atomic deposit program print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cell = new Int32Array(new ArrayBuffer(4));
                Atomics.store(cell, 0, 100);
                Atomics.add(cell, 0, 50);
                Atomics.add(cell, 0, 50);
                console.log(Atomics.load(cell, 0));

                Choose an answer to see the explanation.

              3. Question 3 of 7What does sequential consistency give atomic operations?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why does the message example use an atomic ready flag?

                Choose an answer to see the explanation.

              5. Question 5 of 7What should a short spin loop do when Atomics.pause exists?

                Choose an answer to see the explanation.

              6. Question 6 of 7What does compare-and-swap return when its expected value is stale?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which statement about JavaScript data races is accurate?

                Choose an answer to see the explanation.

              Key takeaways

              • A data race is conflicting shared access without the required atomic ordering; do not depend on any result from it.
              • Atomic operations are sequentially consistent: agents agree on one strict order for those operations.
              • Publish ordinary data before atomically storing a ready flag, then atomically read that flag before consuming data.
              • Feature-detect `Atomics.pause`; use it only as a short spin hint and prefer waiting for long waits.
              • Lock-free compare-and-swap loops retry from the current value when another agent changes the cell first.
              • Messages are usually the clearest tool for rich worker protocols; shared atomics suit small contested state.

              Remember the one-liner.
              Shared memory needs a clear ordering rule; Atomics gives small shared cells one that every agent can agree on.

              Coming next: Parallel patterns with workers, where these small rules become worker pools, message protocols, and efficient data movement.

              CompleteFrontend Clear concepts. Working examples.