Reading in Passes
Don't read a codebase top to bottom. Read it six times, each time looking for one kind of bug.
The story
A candidate gets a 900-line Node service and 60 minutes. They open src/index.ts and start reading, line by line, file by file, noting anything odd.
At minute 40 they're in the fourth of nine files. They've found eleven things: a misspelled variable, two console.logs, some inconsistent naming, a missing await and a magic number. They run out of time before reaching payments.ts, where the interviewer planted the double charge, and orders.ts, where any user can read any order.
Reading top to bottom has two problems. It spends time evenly, when bugs are not spread evenly. And it asks your brain to look for every kind of bug at once, which means it mostly notices the shallow ones.
The fix is to read in passes. Each pass has one question, so your attention has one job, and the passes go where the bugs are.
Part 1: The six passes
The times are for a 60-minute review. Scale them for longer or shorter rounds, but keep the shape: orient briefly, sweep fast, then go deep where the risk is.
Pass 0 · Orient (5 minutes)
Don't read any function bodies yet. Build a map:
- README and
package.json: what the service does, its dependencies (an ORM? a queue? an HTTP client?), and its scripts. Run the tests if you can. Failing tests are free clues. - Entry points: every route, queue consumer, cron job, webhook and CLI command. Bugs live where input enters.
- Folder map: which files hold the domain logic, which hold plumbing.
- The hot spots from the catalogue: money, auth, time, retries, caching, deletes, pagination, anything with a status.
Write down the two or three most important flows: "place order → charge → confirm", "invite user → accept". Those get Pass 2.
Pass 1 · Search sweep (10 minutes)
Before reading closely, search. Many bugs have a textual fingerprint, and a search finds them across the whole codebase in seconds. Run these and note every hit:
| Search for | Usually means | Level |
|---|---|---|
as any, : any, as unknown as, @ts-ignore, !. | Type holes. Check what's being trusted | L2 |
catch { , catch (e) {} , catch (e) { console | Swallowed errors | L2 |
JSON.parse( | Unvalidated data, and possible crashes | L2/L3 |
new Date(, Date.now(), getHours, setMonth | Time-zone and calendar bugs | L2 |
parseInt(, Number(, process.env. | Coercion and missing-config bugs | L1/L2 |
`SELECT / `UPDATE / `INSERT with ${ | SQL built from strings | L5 |
OFFSET, ORDER BY | Pagination drift and unstable pages | L3 |
forEach(async, .then( without .catch | Floating promises | L4 |
let or new Map( at the top of a file | Shared module state | L4/L6 |
Math.random | Weak tokens or IDs | L5 |
innerHTML, dangerouslySetInnerHTML, eval, exec( | Injection and XSS | L5 |
req.body spread into an update, ...req.body | Mass assignment | L5 |
fetch(, axios, SDK clients | Timeouts, res.ok, retries | L3/L6 |
setInterval, setTimeout, .on( | Leaks, lifecycle | L2/L6 |
readFileSync, *Sync( | Blocking the event loop | L6 |
TODO, FIXME, HACK | The author already knew | All |
A hit isn't a bug. It's a place to look. The sweep turns a 900-line codebase into a list of 20–30 suspicious lines.
Pass 2 · Follow the money (15 minutes, L1–L3)
Now read deeply, but only the critical flows from Pass 0. Trace each one end to end: the route, the validation, the service, the repository, the SQL, the external call, the response. At every step ask:
- L1: Is this line right? Off-by-one, falsy checks, missing
await/return, units. - L2: What if the input is empty, zero, negative, huge, duplicated, or in another time zone? What happens on the error path?
- L3: Does what this function returns match what the next one expects? Is the multi-step write in a transaction? Is the cache key complete? Is the tenant filter there?
Pass 3 · Two at once (10 minutes, L4)
Go back over the same flows and mark every await. At each one ask: what else could run here, and what could it change? Look especially for:
- A read, then an
await, then a write based on the read. - "Check, then act" on anything shared: balances, stock, statuses, "does it exist yet".
- Retries and webhooks: what happens if this runs twice?
JS Code Review is the deep version of this pass.
Pass 4 · Attacker (8 minutes, L5)
List every input that crosses a trust boundary: path params, query strings, bodies, headers, cookies, webhook payloads, file uploads, URLs the server fetches. For each, ask:
- Who is allowed to do this, and where is that checked? Look for the ownership check on every
:id. - Where does this value end up? In SQL, a shell command, a file path, HTML, a redirect, a log line, an object merge?
- How big can it be?
Pass 5 · Production (5 minutes, L6)
Picture the code running for a month under real load:
- Every network call: is there a timeout?
- Every collection: can it grow without limit?
- Every list endpoint and query: is there a
LIMIT? - What happens on deploy:
SIGTERM, migrations, old clients? - When it fails at 3 a.m., do the logs say why?
Pass 6 · Write up (7 minutes)
Rank and present (below). Leave time for this. An unranked list of 15 findings scores worse than six findings in the right order with clear fixes.
Part 2: Try the method
Three drills. Use the passes, especially the search sweep. The bug count is shown, so you know when you've found them all. Click a line to flag it, then Check.
Drill 1: a pricing function. One file, warm-up. Mostly L1.
Drill 2: an orders API. Three files. Use the attacker pass and the "two at once" pass.
Drill 3: a session module. One file. Try finding every bug with the search sweep alone before reading closely.
Part 3: Using an AI assistant honestly
If the round allows an assistant, use it, but as a search tool, not an oracle. Assistants are good at listing candidate L1/L2 bugs in one file. They're weaker at bugs that span files (L3), interleavings (L4) and anything that depends on the business rules in the README.
A workflow that works:
- Do Pass 0 yourself. You need the map to judge anything the assistant says.
- Ask for candidates, by category, with line numbers: "List possible bugs in
orders.tsgrouped as: logic, data consistency, concurrency, security, production robustness. For each, give the line and the input or interleaving that triggers it." - Verify every candidate before you say it out loud. Can you name the input, the interleaving or the attacker request that triggers it? If not, it's not a finding yet.
- Look for what it missed. Run Passes 2–4 on the critical flows yourself. Cross-file and concurrency bugs are where you add value.
- Never present a finding you can't explain. Interviewers ask "how would that actually happen?" first.
Part 4: Writing it up
Each finding gets four parts, the format from JS · Reviewing a Codebase:
| Part | Example |
|---|---|
| What and where | "POST /orders/:id/pay can charge the same order twice (routes/orders.ts:28)." |
| How it breaks | "Two requests arrive together, both read pending, both call the provider before either writes paid." |
| Fix | "Claim the order atomically with a conditional UPDATE … WHERE status = 'pending', and send the order ID as the provider's idempotency key." |
| Prevention | "Every state transition should be a conditional update. I'd add a concurrent test for each money path." |
Then rank: security and money first, data corruption next, outages next, wrong results next, everything else last. For Drill 2, that order is: mass assignment with column injection (8, 2), double payment (3), the missing await that marks unpaid orders as paid (4), the IDOR (1), declined charges looking successful (6), no timeout (5), unstable pages (7).
Cheat sheet
| Pass | Question | Time (of 60) |
|---|---|---|
| 0 · Orient | What does it do, where does input enter, where's the money? | 5 |
| 1 · Search | What do the fingerprints say? | 10 |
| 2 · Follow the money | Is each step right, for odd inputs, across files? | 15 |
| 3 · Two at once | What can run at every await? What if it runs twice? | 10 |
| 4 · Attacker | Who's allowed, where does each input go, how big can it be? | 8 |
| 5 · Production | Timeouts, limits, growth, deploys, logs? | 5 |
| 6 · Write up | Ranked findings, four parts each | 7 |
Say it in the interview
"I'll take five minutes to map the entry points and the money flow, then search for the usual fingerprints: any-casts, empty catches, string-built SQL, Math.random, unbounded collections. Then I'll trace place-order and pay end to end, and go back over them for concurrency and authorization. I'll keep the last few minutes to rank what I've found, security and money first."
Go deeper
- The Bug Catalogue: everything each pass is looking for.
- JS · Reviewing a Codebase: the first 15 minutes, triage, and design critique.
- JS Gym: runnable async exercises to practise Pass 3.