JS Code Review — Course Map
How the AI-era code-review interview works, and how to use this section
Read this first. It takes ten minutes. Basics such as the call stack, closures and this are in FE-10 — JavaScript Runtime Internals; this section assumes them.
The Interview Has Changed
When an AI assistant can write a working endpoint in thirty seconds, "write a function that…" tells an interviewer very little. What it cannot tell them is whether you can judge code: see that a function which works on every test will double-charge a user when two requests arrive 40 ms apart, and explain why in a way the team can act on.
So more companies now give you code you didn't write and watch you work through it. The usual tasks:
- Find the bugs. Often planted, often in async code, often invisible to the existing tests.
- Fix broken async code. A race, a missing
await, a retry loop with no backoff. - Critique the design. "This works. Would you ship it? What would you change and why?"
- Write a polyfill.
Promise.all,debounce,promisify, a concurrency pool, from memory, with the edge cases. - Explain trade-offs out loud. Mutex or atomic SQL?
allSettledorall? Chunking or a worker thread?
The common formats
| Format | What happens | What they score |
|---|---|---|
| Take-home repo | A small Node/TS service (a job runner, a webhook handler, a cache) with a README that says "it has bugs; find and fix what you can in 2–3 hours". Sometimes an AI assistant is allowed | Which bugs you found, whether your fixes are minimal and tested, and the write-up explaining each one |
| Live "review this PR" | A 100–300 line diff in a shared editor or GitHub. You comment as if it were a teammate's PR | Priority (do you lead with the double charge or the variable name?), precision, and tone |
| "This test is flaky. Why?" | A test that passes 9 times in 10, plus the code under test | Whether you can name the interleaving that fails, instead of adding a sleep |
| Polyfill round | Implement a standard API on a whiteboard or in a REPL, then answer "what about…" questions | Correct edge cases: empty input, ordering, errors, this, cleanup |
| Debug from symptoms | "Since Tuesday, 1 in 200 users is charged twice." Logs and code are provided | Which checks you choose, in what order, and why |
In all of them, talking is half the score. A correct fix with no explanation looks like luck. A clear explanation of the wrong interleaving followed by a corrected one looks like a senior engineer.
The One Idea Behind the Whole Section
JavaScript runs your code on one thread, but it is concurrent.
- Concurrency means several tasks are in progress at once and take turns on one thread. Request A starts, waits for Postgres, and while it waits, request B runs.
- Parallelism means things run at the same instant on different CPU cores. In JS that only happens with Web Workers or
worker_threads(Chapter 8).
One thread does not make you safe. Every await, every callback, every event handler is a point where other code can run and change the state you just read. Your function is not one atomic step. It is a series of slices, and other requests' slices are interleaved between them:
async function spend(userId, cost) {
const wallet = await db.getWallet(userId); // ← slice 1 ends here; other requests run
if (wallet.balance < cost) throw new Error('insufficient credits');
await db.setBalance(userId, wallet.balance - cost); // ← based on a read that may be stale
}
Two requests can both pass the check against the same balance. This is exactly the SELECT-then-UPDATE race you already know from Postgres, where two transactions both read 100 and both write 20. The fixes are the same family too: make the check and the write one atomic step (UPDATE … SET balance = balance - $1 WHERE balance >= $1), serialise with a lock, or detect the conflict with a version column. If you built a race-safe credit ledger, you have solved this problem once already; this section teaches you to see it in JS and say it out loud. Chapter 5 is all about it.
When you review async code, keep asking one question: "What else can run here, and what could it change?"
The Async Code Review Checklist
This is the one-page version. Each line is expanded in the chapter shown.
| # | Look for | The bug it signals | Chapter |
|---|---|---|---|
| 1 | A read, then an await, then a write based on the read | Check-then-act race; lost update; double spend | 5 |
| 2 | A promise that is created but not returned, awaited or caught | Floating promise: success reported early, errors unhandled | 3, 4 |
| 3 | forEach(async …), .map(async …) without Promise.all | Work not awaited; errors escape | 4 |
| 4 | Promise.all over an unbounded array | Rate limits, pool exhaustion, memory spikes | 7 |
| 5 | A network call with no timeout or AbortSignal | Hung requests that hold sockets, locks and pool slots | 6 |
| 6 | Retries with a fixed delay, or without a cap | Retry storms that keep a struggling provider down | 7 |
| 7 | Responses applied in arrival order (search, autosave) | Stale response overwrites a fresh one | 5, 6 |
| 8 | A cache of promises with no eviction on rejection | One failure cached forever | 3, 7 |
| 9 | A callback that is sometimes called synchronously | Zalgo: ordering depends on cache state | 2 |
| 10 | throw inside a callback, or an await with no try at the boundary | Process crash, or an unhandled rejection that exits Node 15+ | 2, 3, 8 |
| 11 | Listeners, intervals or subscriptions with no removal | Leaks, and handlers firing for unmounted components | 6 |
| 12 | Sync CPU work in a request path, or a loop that only awaits in-memory work | Blocked event loop; every request waits | 1, 8 |
| 13 | Stream code that ignores write()'s return value or splits on chunk boundaries | Memory growth; corrupted parsing | 9 |
| 14 | Work acknowledged before it is persisted (webhooks, queues) | Lost events on crash | 8, 10 |
| 15 | Non-idempotent handlers for anything that can be delivered twice | Double charge, double email | 5, 10 |
How Each Chapter Works
Every chapter from 1 to 11 has the same parts, so you know where to look:
| Part | What it is | How to use it |
|---|---|---|
| How to read this chapter | A short frame and a vocabulary table | Skim it. Come back to the table when a term is unclear |
| Concept parts | Each part is built on a mechanism (which queue, which await, which interleaving), not a rule to memorise | Read slowly. If a rule seems arbitrary, the mechanism is what to remember |
Simulations (sim blocks) | Step-through sims: the event loop, racing requests, concurrency pools, debounce and throttle | Predict before you press step. A sim you click through without predicting teaches nothing |
| Timelines | Two or three requests side by side, step by step, with a question that pauses the timeline until you guess | Answer the question out loud before revealing. This is the interview skill |
| Spot the bug | A 10–40 line realistic snippet, then "What's wrong?" The answer is folded | Give yourself two minutes. Write the bug, the interleaving and the fix before opening the answer. Then compare the "say it" line with what you would have said |
| Build it | A polyfill spec, a folded reference solution, and the edge cases interviewers check | Write it yourself first, in a file, and run it. Then read the edge-case table |
| The Causal Chains | "If X → Y → Z" lines that the reader turns into click-to-reveal self-tests | Cover the consequence and say it. These are your revision cards |
| Review checklist | 5–10 things to grep for when the topic shows up in a codebase | Use it on your own code. Then on the gym repos |
The gym
The section has a practice repo, js-gym/, with small services that have planted bugs and tests that don't catch them. Treat each exercise as a take-home: set a timer, read the code cold, write down every bug with its interleaving, fix it, and add a test that would have caught it. Then check the answer key. Chapter 12 explains how to run it.
| Exercise | Theme | Practises chapters |
|---|---|---|
01-search-box | Search-as-you-type | 5, 6 |
02-credit-wallet | Credit ledger with a fake async DB | 5 |
03-llm-client | Multi-provider LLM client | 6, 7 |
04-job-runner | In-process job queue | 7, 8 |
05-webhook-handler | Payment webhooks | 5, 8, 10 |
06-cache-layer | Read-through cache | 3, 5 |
07-event-bus | Pub/sub | 2, 6 |
08-stream-parser | LLM SSE stream | 9 |
09-legacy-callbacks | Callback-style module | 2 |
10-polyfill-kata | Build, not fix: stubs plus tests | 2, 3, 7, 11 |
review-drills/ | 30-minute reviews of larger files | 10 |
Suggested Study Order
If your interviews are soon, do the highest-yield chapters first. Race conditions and async/await traps are planted in almost every review exercise; concurrency control and failure handling are what turn a "found the bug" into a "senior" score.
- Chapter 5 — Race Conditions. The core skill. Do every timeline.
- Chapter 4 — async/await Traps. The most common planted bugs.
- Chapter 7 — Controlling Concurrency. Pools, rate limits, retries with backoff and jitter. Your LLM failover experience maps straight onto this.
- Chapter 8 — The Node Process Under Failure. Crashes, graceful shutdown, and real parallelism.
- The gym:
02-credit-wallet,03-llm-client,04-job-runner, thenreview-drills/. - Chapter 11 — Polyfills: drill
Promise.all,debounce,asyncPoolandpromisifyuntil you can write them without looking. - Chapter 10 — Reviewing a Codebase: how to run a 30-minute review and defend your designs.
- Then the rest in order: 1, 2, 3, 6, 9.
With more time, read 1 through 11 in order and do each gym exercise after the chapters it practises.
All Chapters
| # | Chapter | After it, you can catch… |
|---|---|---|
| 0 | Course map (this page) | — |
| 1 | The Event Loop Under Pressure | Ordering puzzles, microtask starvation, loops that never yield, long tasks, setImmediate vs setTimeout flakiness |
| 2 | Callbacks Done Wrong | Zalgo, callbacks called twice or never, throws that crash the process, lost this, broken promisify |
| 3 | Promises in Real Code | Floating promises, the construction antipattern, swallowed errors, unhandled rejections, wrong combinator choice |
| 4 | async/await Traps | Missing await, forEach(async), accidental serial work, return vs return await, errors outside try |
| 5 | Race Conditions in Single-Threaded JS | Check-then-act across await, double charge, double submit, stale responses, lost updates |
| 6 | Cancellation and Cleanup | Missing timeouts, unaborted losers, leaking listeners and intervals, React effects without cleanup |
| 7 | Controlling Concurrency | Unbounded Promise.all, pool bugs, fail-fast that keeps running, retry storms |
| 8 | The Node Process Under Failure | Crashes from unhandled errors, lost work on shutdown, blocked event loops, when to use worker_threads |
| 9 | Streams and Async Iterators | Ignored backpressure, chunk-boundary bugs in SSE parsing, readers that leak on abort |
| 10 | Reviewing a Codebase | How to prioritise findings, write them up, and defend a design under questioning |
| 11 | The Polyfill Set | Every polyfill round: bind, array and object methods, event emitters, promise combinators, debounce, throttle |
| 12 | The Gym | Practice repos with planted bugs and answer keys |
What's Next
Start with Chapter 1 — The Event Loop Under Pressure if you are going in order, or jump to Chapter 5 if your interview is this week.