completefrontend
Open lab My progress
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
    Spot a data raceTell a plain read-add-write apart from one atomic step, and explain why its result can't be trusted.
  • 02
    Use the order of atomic stepsExplain the one order that all atomic steps share, and hand over a value with an atomic ready flag.
  • 03
    Pick a way to coordinateCompare a short spin lock, sleeping until a cell changes, and a compare-and-swap loop that tries again.

What the memory model promises

A worker is a second JavaScript program that runs at the same time as your page. Each worker has its own variables. Usually, nothing is shared.

A SharedArrayBuffer changes that. It is a block of bytes that the page and its workers can all read and write. When two programs touch the same bytes at the same time, you need rules for what each one sees. Those rules are the memory model.

A few words you will see in this lesson:

  • An agent is one running JavaScript program, like the page or one worker.
  • A cell is one numbered spot in shared memory, like cells[0].
  • An atomic operation is one step that no other agent can split in the middle. The Atomics functions, like Atomics.add, are atomic.
  • A plain read or write is the normal kind, like cells[0] = 5. It is not atomic.
The main idea

JavaScript's memory model is the set of rules for shared memory. It says which values an agent is allowed to see when agents read and write the same cells. All atomic operations happen in one order that every agent agrees on. Plain reads and writes that clash have no such order. They can give surprising values, but JavaScript never crashes because of them.

This lesson follows SharedArrayBuffer & Atomics. That lesson showed the functions. This one explains the order rules behind them. The next lesson, Parallel patterns with workers, uses these rules to build groups of workers.

Data races

A data race happens when all of these are true:

  • Two agents use the same shared cell.
  • At least one of them writes to it.
  • At least one of the two uses a plain read or write.
  • Nothing decides which one goes first.

A line like cells[0] += 1 looks like one small step. It is really three steps: read the old value, add one, and write the new value. Another agent can act between those steps.

Two plain 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);

This code acts out two agents, A and B, in one fixed order. Here is what each line does:

  • Line 1 starts the balance at 100.
  • Line 2 is A reading the balance. A keeps 100.
  • Line 3 is B reading the balance. B also keeps 100.
  • Line 4 is A writing its deposit: 100 + 50, so 150.
  • Line 5 is B writing its deposit. B still uses its old 100, so it also writes 150.
  • Line 6 prints 150. One deposit of 50 is lost.
A reads 100, then B reads 100. A writes 150, then B also writes 150. The balance box ends at 150, not 200.1. A reads 1003. writes 150150balance2. B reads 1004. writes 150balance is 150, not 200
Both agents read 100 before either one writes. Each writes 100 + 50, so the balance ends at 150 instead of 200.

We picked this order on purpose, so the lost deposit is easy to see. A real race does not always lose exactly one deposit. Real agents can run in other orders, and the rules allow even stranger values. So the rule is simple: never trust a result that comes from a data race.

Real-life analogyTwo ATMs change one balance

Two people put money in at two ATMs at the same moment. Both screens show 100. Both add 50. Both save 150. One deposit disappears, because neither ATM held the balance while it made its change.

In real life: Both screens show 100
In JavaScript: Both agents read the same shared cell
In real life: Each person adds a 50 deposit
In JavaScript: Each agent adds to the value it read
In real life: The second ATM writes over the first
In JavaScript: A plain write can wipe out another write
In real life: The bank lets only one change happen at a time
In JavaScript: An atomic update happens as one step, in one order

Step through a lost deposit

The replay below runs the same balance code, one line at a time. It shows one bad order of plain reads and writes. It is a recording of this small model. It does not watch real workers, and it cannot tell you the order your workers will use.

Step through a lost deposit
Step 0 of 6Ready
Your turn: follow the blue line

Step through one fixed order that loses a deposit. This is a small model, not two real workers.

Running in
  1. script
Next: line 1
const a = balance;const b = balance;balance = a + 50;balance = b + 50;console.log(balance);
CallStoreChangeResultBlue line: runs next. 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. A and B each keep 100 in their own variable. Then A writes 150. B still holds its old 100, so B also writes 150, over the top of A's value. The deposit was lost between B's read and B's write. Addition worked fine; the order was the problem.

Playground: pick the order of the steps
Two reads and two writes to put in orderJavaScript
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);
Your orderbalance 150
A reads
B reads
A writes
B writes
what happenedA read 100 | B read 100 | A wrote 150 | B wrote 150

Both agents have read and written.

Step 4 of 44 of 4 steps picked

Plain steps: A read 100; B read 100; A wrote 150; B wrote 150. Final balance: 150. This is one order you picked. Real workers may use a different one.

This is a small model with an order you pick. Real workers may run in any order, so never rely on a plain shared update.

Now pick the steps in a different order. If A reads and writes before B reads, the plain model reaches 200. Switch to atomic deposits: each write becomes one Atomics.add, which reads, adds, and writes as one step. Then the two deposits always reach 200 in this model.

One shared order for atomic steps

Atomic operations follow a rule called sequential consistency. It means:

  • All atomic operations happen one at a time, in one list.
  • Every agent agrees on that list.
  • Each agent's own atomic steps appear in the list in the same order as in its code.

Picture one shared notebook. Every atomic step is written on its own line, and every agent reads the same notebook.

Two atomic deposits happen one at a timePop 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 makes one cell that holds a whole number. An Int32Array is a list of whole numbers stored in bytes.
  • Line 2 uses Atomics.store to put 100 in cell 0.
  • Lines 3 and 4 each use Atomics.add to add 50. No other atomic step can land in the middle of either add.
  • Line 5 uses Atomics.load to read the cell. It prints 200.
Line 3 adds 50 and turns 100 into 150. Then line 4 adds 50 and turns 150 into 200. The cell box ends at 200, so both deposits are kept.Atomics.add(cell, 0, 50)1. line 3 adds 50100 to 150200cell2. line 4 adds 50150 to 200both deposits kept
Each Atomics.add is one step that nothing can split. Line 3 runs fully, then line 4, so the cell ends at 200.

This small example uses a normal ArrayBuffer, so it can run in the page. With workers you would use a SharedArrayBuffer instead. The order rule is the same.

Atomic steps protect only the cells they touch. They do not protect your whole app. So give each cell one clear job, such as a flag, a counter, or a lock. Do not mix plain and atomic access on the same cell when agents compete for it. The specification recommends programs with no data races. In those programs, everything happens in an order you can follow.

The table below compares four ways to use shared cells. Read each row as: "this kind of access is this, follows this order rule, and is good for this job."

Four ways to use a shared cell
Kind of accessWhat it isIs there one order?Use it for
Plain accessA normal read or write, like cells[0] = 5.No. Agents don't agree on one order for plain steps.Data that only one agent touches at a time.
Atomic accessAn Atomics call on one cell of whole numbers.Yes. All atomic steps happen in one order.Flags and counters that agents compete for.
A lockOne atomic cell that lets one agent at a time in.Yes. Taking and giving back the lock are atomic steps.Guarding a few plain steps that must not be split.
Compare-and-swap loopSwap in a new value only if the cell still holds the old one; else try again.Yes. Each compare-and-swap is one atomic step.Changing a cell without a lock, when trying again is fine.

Share a value with a ready flag

Here is a common way for one agent to hand a value to another. It uses two cells:

  • A data cell holds the value. It uses plain reads and writes.
  • A ready flag cell says "the data is there". It uses atomic steps only. 0 means not ready, and 1 means ready.

The writer fills the data cell first. Then it sets the flag to 1. The reader checks the flag first. It reads the data only after it sees 1.

Write the data, then set 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]);}
  • Line 1 makes two cells.
  • Lines 2 and 3 name them: cell 0 is data, and cell 1 is ready.
  • Line 4 puts 42 in the data cell, with a plain write.
  • Line 5 uses Atomics.store to set the ready flag to 1.
  • Line 6 uses Atomics.load to read the flag.
  • Only when the flag is 1 does line 7 read the data. It prints 42.
Two boxes. The data box holds 42 and the ready box holds 1. Step 1: write 42 into data. Step 2: then store 1 in ready. Step 3: the reader sees 1, then reads 42.42data1. write 42 first1ready2. then store 13. reader sees 1, then reads 42
The writer fills data first, then sets ready to 1. The reader checks ready first, and reads 42 only after it sees 1.

The flag works because both agents use atomic steps on it. That gives them one shared moment to agree on: "ready became 1". Everything the writer did before that moment is there for the reader after it sees the 1.

This is a small teaching example, not a full mailbox. A real worker setup also has to decide:

  • Which agent is allowed to write each cell.
  • Whether the data can be replaced, and when.
  • How a waiting agent finds out that the flag changed.

For job details with many fields, use postMessage, which sends a copy of a value to a worker. Use shared cells only when a small shared value really helps.

Spin locks and the pause hint

A lock lets only one agent at a time use some shared data. The lock is one cell:

  • 0 means unlocked: anyone may take it.
  • 1 means locked: one agent is using the data.

A spin lock keeps trying to take the lock, again and again, instead of going to sleep. That can make sense for a very short wait inside a worker. But every failed try keeps the processor busy.

To take the lock, the code uses Atomics.compareExchange. This is a compare-and-swap: "if the cell still holds this value, swap in that value". It does the check and the swap as one step, and it gives back the value the cell had before.

A small lock with an optional pause hintPop 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 to 3 call Atomics.pause only if it exists. typeof gives the kind of a value as text, such as "function".
  • Lines 5 to 7 try to take the lock. If the cell holds 0, it becomes 1. The call gives back the old value, so the result is true only when the old value was 0.
  • Lines 9 to 11 give the lock back by storing 0.
  • Line 13 makes the lock cell. It starts at 0.
  • Line 14 takes the lock and prints true.
  • Line 15 gives it back, and line 16 prints 0.

Atomics.pause returns nothing you can use. It is only a hint to the processor: "I am waiting in a loop." On some computers it does nothing at all. It exists in Node 24 and newer browsers, but not in Node 22. That is why the code checks for it first.

For a long wait inside a worker, use Atomics.wait instead. It puts the worker to sleep until a cell changes. Never spin on the page's main thread: the page freezes while the loop runs.

Short waits only

Spin only for a short, limited number of tries, and only when you have measured that agents rarely wait for the lock. If the lock is still taken, wait or change the design. A loop that spins forever can steal processor time from the agent that holds the lock, so it takes even longer to give the lock back.

Try again instead of locking

A lock-free design changes shared cells without a lock. It uses compare-and-swap in a loop:

  1. Read the cell.
  2. Work out the new value.
  3. Swap it in, but only if the cell still holds the value you read.
  4. If another agent changed the cell first, go back to step 1.

One agent may have to try again. But each time a swap works, the work moves forward, so no agent is ever stuck waiting for a lock. Lock-free is a choice for some designs. You don't need it for every counter.

Read a counter, then swap in one morePop 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 1 makes one cell.
  • Line 2 stores 3 in it.
  • Line 3 reads the cell and keeps 3 in observed.
  • Line 4 asks: "if the cell still holds 3, put 4 in".
  • Nothing else changed the cell, so line 5 prints 4.

If another agent changes the cell first, the swap does nothing. It gives back the value that is really in the cell now. The loop reads again, works out a new value, and tries again.

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

Step through a compare-and-swap that has to try again. The change from another agent is written into the code, so the retry is easy to see.

Running in
  1. script
Next: line 2
const cell = new Int32Array(new ArrayBuffer(4));const observed = Atomics.load(cell, 0);Atomics.store(cell, 0, 4); // another agent changes the cellconst firstOld = Atomics.compareExchange(cell, 0, observed, observed + 1);const fresh = Atomics.load(cell, 0);Atomics.compareExchange(cell, 0, fresh, fresh + 1);console.log(Atomics.load(cell, 0));
CallStoreChangeResultBlue line: runs next. 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 adds a change from another agent on line 4, between the read and the first swap. The cell is now 4. So the first swap, on line 5, still expects 3. It gives back 4 and changes nothing. The retry reads 4 on line 6 and swaps in 5 on line 7. Line 8 prints 5.

Left, try 1: the cell holds 4 but the swap expects 3, so nothing changes and it returns 4. Right, try 2: the swap expects 4, the cell holds 4, so it puts 5 in the cell.try 1: expects 34cell4 is not 3: no swaptry 2: expects 45cell4 matches: 5 goes in
Another agent changed the cell from 3 to 4. Try 1 still expects 3, so nothing changes. Try 2 expects 4, so 5 goes in.

Real lock-free lists and stacks have harder problems, such as items that are removed while another agent is still using them. That is why a single counter is the right first example.

Two real workers, exact totals

The examples so far act out one chosen order. Now for real workers. Node has a module called worker_threads that starts workers. The lesson's tests run two programs with it, and both always give the same answer.

In the first program, two workers each run Atomics.add one thousand times on one shared counter. The total is always exactly 2000.

Node only: two workers add to one 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();}
  • Line 1 loads the worker tools. The same file runs as the main program and as each worker; isMainThread tells them apart.
  • Line 3 makes 4 shared bytes: room for one counter.
  • Line 4 starts two workers and gives each the shared bytes.
  • Lines 5 and 6 wait until both workers stop, then print the counter.
  • In each worker, line 8 makes a view of the shared bytes, and line 9 adds 1 a thousand times with Atomics.add.

The test saves this program to a short-lived file, runs it, and checks that it prints 2000.

Node only: a spin lock guards a 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();}

The second program uses two cells. Cell 0 is the lock, and it only gets atomic steps. Cell 1 is a counter that gets a plain write.

  • Line 10 keeps trying until it takes the lock.
  • Line 11 adds 1 to the counter with a plain write. This is safe, because only the lock holder can be here.
  • Line 12 gives the lock back.

Again, the two workers print exactly 2000.

Neither test tries to prove that a plain race loses a deposit. That result changes from run to run, so a test for it would sometimes pass and sometimes fail. The lesson uses the fixed-order model to show the danger, and real workers only for results that never change.

Pick the simplest tool that works

Start with messages. A group of workers can get each job with postMessage and send the result back the same way. Add shared memory only for a small value that many agents touch often, and only after you have designed and tested it. Good examples:

  • A counter of finished jobs.
  • A ready flag.
  • The position in a small shared queue.

For every shared cell, answer these plain questions:

  • What is this cell for? Write it down in a comment.
  • Is it an atomic flag or plain data? Keep those in separate cells.
  • Which agent is allowed to write it?
  • When may a reader look at it?
  • How does a waiting worker find out it changed?

These questions stop most shared-memory bugs before you think about speed.

Sort the shared-memory tools
  • cells[0] reads a shared value
  • cells[0] += 1 in two workers
  • Atomics.add(cells, 0, 1)
  • Set a ready flag with Atomics.store
  • Keep calling compareExchange until you get the lock
  • Try again when compare-and-swap gives back a different value
Try it yourself
0 of 6 correct

Put each card in the group that describes it: a plain access, an atomic operation, or a way to coordinate agents.

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.” A race can print the right number in one run and the wrong one in the next. You can't trust it either way.
  • “A data race can make JavaScript crash or do anything.” No. The rules list which values a race may give. The values can surprise you, but the program keeps running.
  • “Atomic protects everything near it.” An atomic step protects only the one cell it touches. How the other cells stay safe is up to your design.
  • “Atomics.pause is a sleep.” It is only a hint while the code loops, and it may do nothing at all.
  • “Lock-free means no retries.” A compare-and-swap loop tries again whenever another agent changed the cell first.

The table below lists four terms that are easy to mix up. Read each row as: "this term means this, and it does not mean that."

Four terms and what they don't mean
TermWhat it meansWhat it does not mean
Data raceTwo agents clash on one shared cell, and at least one uses a plain read or write.That JavaScript crashes or can do anything.
Sequential consistencyAll atomic steps happen in one order that every agent agrees on.That plain reads and writes need no planning.
Atomics.pauseA hint to the processor while code loops. Check that it exists first.A sleep, a pause for other code, or a way to make code correct.
Lock-freeChanging cells with compare-and-swap and trying again, without a lock.That it is always faster or always simpler.

The specification gives careful advice: write programs with no data races. Use atomic steps every time agents compete for a cell. Never use plain access and atomic access on the same cell that agents compete for.

Practice exercises

Exercise 1 · Warm-upPredict the lost deposit

This code acts out two agents in one fixed order. 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 cell the reader checks first

      Which cell must the reader check with an atomic step before it reads the data?

      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 tool for a long wait

        Your worker may wait many milliseconds for a new job in a queue. Which tool is better than a loop that spins with no limit?

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

          Exercise 5 · ChallengeRead a successful compare-and-swap

          In this example, no other agent touches the cell. What final number does it 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 · ChallengePlan a group of photo workers

            A photo website sends work to a group of workers. Each job has file details, options, and possible errors. What should it use to send those job details?

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

              Check your understanding

              For each question, find the shared cell first. Then ask: is each read or write plain or atomic, and which agent sees what?

              Memory model quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which of these can cause a data race?

                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 steps?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why does the ready flag example set the flag with an atomic step?

                Choose an answer to see the explanation.

              5. Question 5 of 7In a short spin loop, what should you do if Atomics.pause exists?

                Choose an answer to see the explanation.

              6. Question 6 of 7A compare-and-swap expects 3, but another agent already changed the cell to 4. What does it give back?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which sentence about JavaScript data races is true?

                Choose an answer to see the explanation.

              Key takeaways

              • A data race is when agents clash on one shared cell and at least one uses a plain read or write. Never trust a result from a race.
              • Atomic steps happen one at a time, in one order that every agent agrees on. This is called sequential consistency.
              • To share a value, write the data first, then set a ready flag with Atomics.store. The reader checks the flag with Atomics.load before it reads the data.
              • Check that Atomics.pause exists before you call it. Use it only as a hint in a short spin. For long waits, use Atomics.wait.
              • A lock-free compare-and-swap loop tries again with the new value when another agent changed the cell first.
              • Messages are usually the clearest way to send work to workers. Shared atomic cells suit small values that many agents touch.

              Remember the one-liner.
              Shared memory needs a clear order; Atomics gives small shared cells one order that every agent agrees on.

              Coming next: Parallel patterns with workers. There, these small rules grow into groups of workers, message plans, and fast ways to move data.

              CompleteFrontend Clear concepts. Working examples.