Class bug cổ điển của hệ thống file / OS: TOCTOUtime-of-check to time-of-use. Bạn kiểm tra điều kiện, rồi mới dùng kết quả kiểm tra đó; giữa hai bước thế giới đã đổi.

Trong JS single-thread, người ta hay nghĩ “không race”. Sai chỗ: mỗi await là điểm nhường — task khác (request khác, job khác, click khác) có thể chạy logic tương tự trên cùng resource. Không cần hai CPU; chỉ cần hai async flow xen kẽ.

Demo: TOCTOU — hai request “check balance rồi trừ”.

Pattern hỏng

async function withdraw(userId, amount) {
  const bal = await db.getBalance(userId); // CHECK
  if (bal < amount) throw new Error("insufficient");
  // ← await khác có thể xen vào đây
  await db.setBalance(userId, bal - amount); // USE (dựa trên bal cũ)
}

Hai request song song, balance = 100, mỗi cái rút 80:

A: get → 100, ok
B: get → 100, ok
A: set 100-80 = 20
B: set 100-80 = 20   // cả hai “thành công”, đã rút 160 từ 100

Cùng họ với:

  • if (!await exists(id)) await create(id) → duplicate
  • if (!inflight.has(k)) { ... set } không atomic với async work (single-flight đúng phải set promise trước await work — xem single-flight)
  • Idempotency “get key miss rồi charge” không lock — double charge
  • Coupon “còn lượt?” rồi redeem — oversell

Vì sao single-thread vẫn dính

Event loop không chạy hai hàm đồng thời trên một call stack — nhưng sau await db.getBalance, continuation của A bị xếp hàng; B có thể chạy hết check của mình trước khi A resume setBalance. Đó là interleaving trên shared state, không phải data race memory kiểu C++.

// “An toàn” giả: không await giữa check và act… 
// …nhưng I/O bắt buộc phải await. Vấn đề nằm ở shared mutable state + yield.

Cách chữa (từ rẻ đến chắc)

1. Atomic operation ở store

// SQL
UPDATE accounts SET balance = balance - $1
WHERE id = $2 AND balance >= $1;
-- rowCount === 0 → insufficient hoặc conflict
// Redis
// Lua / MULTI: GET + điều kiện + DECRBY một atomic unit

Check và use gộp một vòng phía engine — không mang bal về app rồi tính.

2. Optimistic concurrency (version / ETag)

async function withdraw(userId, amount) {
  for (let i = 0; i < 5; i++) {
    const row = await db.get(userId); // { balance, version }
    if (row.balance < amount) throw insufficient;
    const ok = await db.updateWhere(
      userId,
      { balance: row.balance - amount, version: row.version + 1 },
      { version: row.version } // CAS
    );
    if (ok) return;
    // conflict → đọc lại
  }
  throw new Error("conflict");
}

Ai ghi sau với version cũ fail → retry. Đúng mental model compare-and-swap.

3. Pessimistic lock

BEGIN;
SELECT balance FROM accounts WHERE id = $1 FOR UPDATE;
-- check + update trong transaction
COMMIT;

Đơn giản về correctness; dễ contention; timeout lock cần nghĩ.

4. Unique constraint / “insert claim”

// thay vì if (!exists) create
INSERT INTO redemptions (coupon_id, user_id) VALUES ($1, $2);
// unique(coupon_id, user_id) → lần 2 bị 23505 → coi như đã redeem

Rất hợp idempotency record: “claim key” bằng insert unique, không phải check-then-insert.

In-process: mutex async

Khi state chỉ trong một process (rate limit memory, in-memory wallet demo):

const locks = new Map(); // key → Promise chain

function withLock(key, fn) {
  const prev = locks.get(key) || Promise.resolve();
  let release;
  const gate = new Promise((r) => { release = r; });
  const next = prev.then(() => gate);
  locks.set(key, next);

  return prev.then(fn).finally(() => {
    release();
    if (locks.get(key) === next) locks.delete(key);
  });
}

await withLock(userId, () => withdrawUnsafe(userId, amount));

Chỉ gộp trong một process — multi-instance vẫn cần DB/Redis lock. Giống bài học single-flight local vs distributed.

Checklist review PR

  • await giữa “đọc quyết định” và “ghi side-effect”?
  • Hai request cùng resource có thể chồng không?
  • Fail path có để state nửa vời không?
  • Test có case song song (hai Promise cùng lúc) không — test tuần tự không bắt được TOCTOU.

Một câu để nhớ

if (await check) await act không atomic. Mỗi await là khe hở. Gộp check+act vào một primitive atomic, CAS + retry, lock, hoặc unique claim — đừng tin “mình vừa đọc xong thì vẫn đúng”.