Get unstuck

Hint 1: Checking twice still races

The check and mutation must share a transaction or conditional-write boundary. Repeating read() leaves a gap.

Hint 2: Use the supplied operation

debitIfEnough owns the balance invariant. Your handler validates the amount and handles its result.

Reveal the reference patch and explanation
Fixedcheckout.mjsJavaScript
export async function purchase(store, amount) {
  if (!Number.isSafeInteger(amount) || amount <= 0) return 'declined';
  return await store.debitIfEnough(amount) ? 'paid' : 'declined';
}

The unsafe schedule reads 100 twice, then subtracts 80 twice: final balance is -60. The atomic operation lets one 80-cent request pay, declines the other and leaves 20. The report sorts the two answers, so declined/paid does not say which caller won. Two 40-cent requests both pay and also leave 20. Rejecting negative, zero, fractional, string or unsafe-integer amounts before debit prevents the handler from creating credit or losing numeric precision. The supplied primitive owns atomicity; this workshop does not implement a database transaction.

Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.

Inspect the actual test harness

The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.

Download cases.mjs
How the browser sandbox runs your code

Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.

The same as in current Node.js:

  • ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag

Added to the sandbox so that it behaves like Node.js:

  • structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
  • URL and URLSearchParams, parsed by your browser’s own URL implementation
  • TextEncoder and TextDecoder for UTF-8
  • Intl.Collator and a locale-aware String.prototype.localeCompare
  • console.log and related methods, whose output appears with your results

Not available in the browser sandbox:

  • Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
  • Node.js modules and globals: import, require, process and Buffer
  • Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
  • Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
  • using declarations, Symbol.dispose and DisposableStack (explicit resource management)
  • Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace

Use the lesson in your own system

The shared store models an atomic database operation by checking and subtracting without an intervening await. It does not establish isolation of a real database or payment provider. The linked transaction guide explains the database invariant and the separate runnable checkout project supplies a SQLite exercise. Production payments also require idempotency, account authorization and recovery.

Your next task: Which database constraint, conditional update or locked transaction enforces your real balance invariant?

OWASP business logic testing

Related guide: Preserve business invariants under concurrent requests