The review checklist
The index lesson gave the reading order; the AI bug catalog gave 24 concrete patterns. This page turns both into one checklist you can actually run against a diff, grouped into the same six areas the rest of this course uses: event loop & async, streams & resources, HTTP boundaries, modules & packages, errors & process, and testing & tooling. Every item links to the lesson that explains the “why” in full.
Event loop & async
Section titled “Event loop & async”- Is every
Promise-returning call eitherawaited or given an explicit.catch()? See Async/Await. - Does anything pass an
asynccallback toArray.prototype.forEach,.map()used for side effects, or a similar method that never awaits its callback? See Async/Await. - Does a
Promise.all(...)run over a list whose length is not bounded by the code itself (user input, a full table scan)? See Promises. - Is
setTimeout(fn, 0)used anywheresetImmediateorprocess.nextTickmore accurately describes when the work should actually run? See libuv and the Threadpool. - Does any synchronous work — a busy loop,
readFileSync, a largeJSON.parse— run inside a path that also needs to serve other requests concurrently? See Call Stack.
Streams & resources
Section titled “Streams & resources”- Does every
EventEmitter-based object (a server, a socket, a custom class) have an'error'listener registered? See EventEmitter. - Are listeners registered once and removed when their resource is torn down, rather than added fresh on every request? See EventEmitter.
- Does every
.pipe()call have an explicit error-handling plan, or wouldstream.pipeline()be the safer choice? See Pipe & Pipeline. - Are incoming chunks decoded independently with
.toString(), instead of through aStringDecoderor full accumulation first? See Buffer. - Is
Buffer.concat([...])called once per incoming chunk inside a loop, instead of once at the end over a collected array? See Buffer.
HTTP boundaries
Section titled “HTTP boundaries”- Is every
res.end()call guarded (if (res.writableEnded) return;or equivalent) so it can never fire twice for one response? See HTTP Server. - Are all headers set before the first
res.write()/res.end(), never after? See HTTP Server. - Is the request body size-limited before it is fully buffered? See JSON API.
- Is every
JSON.parseon untrusted input wrapped in atry/catchthat returns a clean400? See JSON API. - Does every
fetchcall checkresponse.okbefore trusting the body, and pass an explicit timeout (AbortSignal.timeout(...))? See Requests.
Modules & packages
Section titled “Modules & packages”- Does the file mix
require()/import, or reference__dirname/__filenameinside a.mjs/ESM file? See Modules. - Is there a committed lockfile, and does the Dockerfile run
npm cirather thannpm install? See Dependencies. - In
package.json’sexportsmap, does a specific condition liketypescome before generic ones likedefault? See package.json.
Errors & process
Section titled “Errors & process”- Does the code call
process.exit()anywhere it could instead return normally or setprocess.exitCode? See process.env. - Is
console.logcarrying information a log pipeline needs to parse, instead of one structured JSON object per line? See Debugging. - Does any
child_process.exec,eval, ornew Functioncall run on a string assembled from untrusted input? See Security Model. - Is a value from
process.envcompared or used arithmetically without first converting it from a string? See process.env.
Testing & tooling
Section titled “Testing & tooling”- Does every
test()block contain at least one assertion that is actually reached? See Built-in Test Runner. - Is every
mock.method/mock.fnapplied at the same call site the code under test actually uses, not a reference captured earlier? See Built-in Test Runner. - Do any tests depend on real timers finishing instead of
mock.timerswith an explicittick()? See Built-in Test Runner.
The one-command grep pass
Section titled “The one-command grep pass”None of the checklist above requires reading every line first — most of it can be narrowed down with one grep over a diff or a whole source tree, using the exact strings this course’s catalog keys on:
grep -rnE "forEach\(async|readFileSync|\.pipe\(|process\.exit\(|catch *\{\}|shell: *true|exec\(|eval\(|JSON\.parse\(|res\.end\(" src/Output (verified on Node 24)
forEach\(async: true
readFileSync: true
\.pipe\(: true
process\.exit\(: trueEvery one of the four patterns in this small sample matches — the same regular expressions the one-liner above uses, just run as JavaScript instead of shell grep for a runnable demo. A real grep hit is not proof of a bug (a .pipe() call might already have careful error handling elsewhere), only a place to slow down and check against the matching checklist item.
The short version
Section titled “The short version”Paste this into a PR description or a review comment as a starting checklist — six lines, no links, meant to be checked off by hand:
[ ] Every promise awaited or .catch()-ed; no forEach(async ...); Promise.all has a bound[ ] Every EventEmitter/stream has an 'error' listener; listeners removed, not accumulated[ ] Every res.end() guarded against firing twice; body size limited; JSON.parse try/catch'd; fetch checks .ok + timeout[ ] No require/import mixing, no __dirname in ESM; lockfile committed; exports specific-before-generic[ ] No process.exit() mid-flight; structured logs, not console.log; no exec()/eval() on untrusted strings[ ] Every test() has a reached assertion; mocks applied at the real call site; no real-timer testsMental Model
Section titled “Mental Model”A checklist is a compressed, reusable form of the reading habits from the index lesson — each item exists because a real pattern in the AI bug catalog has bitten someone before. It speeds up a review; it does not replace reading the code the checklist cannot yet anticipate.
Trade-offs
Section titled “Trade-offs”| Approach | Benefit | Cost |
|---|---|---|
| The full six-group checklist | Traces every item back to a lesson with the full explanation | Long enough that skimming it under time pressure defeats the point |
| The six-line short version | Fast enough to actually paste into every PR | Compresses away the “why,” so a reviewer who disagrees with an item has to go look it up anyway |
| The one-line grep | Finds the fastest, most literal hits in seconds, even across a huge diff | Only catches patterns with a stable, greppable string — most of the catalog’s “none — only a human” patterns have no such string |
Common Misconceptions
Section titled “Common Misconceptions”- “Passing the grep means the diff is clean.” The grep pass finds only the patterns that have a stable string to search for — roughly a third of the AI bug catalog’s 24 patterns. The rest need an actual read.
- “A checklist this long is slower than just reading the code carefully.” For a reviewer who has internalized the index lesson’s four habits, the checklist is a fast cross-check afterward, not a replacement for reading — used well, it takes seconds per item because the reviewer already knows what to look for.
- “If an item isn’t on this checklist, it isn’t worth checking.” This checklist mirrors one catalog of 24 known patterns; a codebase’s specific domain (payments, auth, PII) always has boundary and suppression risks a generic checklist cannot enumerate in advance.
Reading AI-generated code
Section titled “Reading AI-generated code”Run the grep pass first, then work through the six groups in order for anything grep did not already flag — the same triage a reviewer uses on the AI bug catalog itself, just condensed into checkboxes instead of full explanations.
🐛 Spot the bug
// Reviewer's note: "grep found nothing, so this file is clean."async function chargeCustomer(order) {const total = order.items.reduce(function (sum, item) { return sum + item.price; }, 0);return billingClient.charge(order.customerId, total); // no timeout, no .ok-equivalent check, no retry limit}
chargeCustomer(currentOrder); // BUG: floating promise -- not forEach-shaped, so the grep list never catches itShow the answer
None of the ten grep strings this lesson lists appear anywhere in this file — no forEach(async, no exec(, no catch {}. But chargeCustomer(currentOrder); at the bottom is a completely ordinary floating promise: if billingClient.charge ever rejects (a declined card, a network error), nothing catches it, and the process crashes on an unhandled rejection. The checklist’s event-loop-and-async group item — “is every Promise-returning call either awaited or given an explicit .catch()?” — still catches this on a real read, which is exactly why the grep pass is a first pass, never the whole review.