GitHub's Octoverse 2026 report says that by September, more than 40% of pull requests started with an agent, up from 17% in March. Stack Overflow's 2026 survey found that 73% of developers who use AI coding tools use them daily.
So a growing share of the code you approve was drafted by something that has never run your app.
This post is for anyone who reviews that code, which is now most of us. Each example below looks correct, would pass a skim, and is broken. All of them run in Node 22. Paste them into a file and see for yourself.
I'm not claiming these are the bugs AI makes most often. Nobody has published that data. They're here because they're the kind of plausible code that slips through when the reviewer is skimming instead of reading.
1. async inside forEach doesn't wait
const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const rows = []; const save = async (u) => { await sleep(10); rows.push(u); }; async function saveAll(users) { users.forEach(async (u) => { await save(u); }); return rows.length; } console.log(await saveAll(['a', 'b', 'c'])); // 0 forEach ignores the promises your callback returns. saveAll resolves before a single row is saved. In a request handler, that means responding "saved" while the writes are still in flight, or silently failing.
Fix: decide whether the writes can run in parallel.
// parallel await Promise.all(users.map((u) => save(u))); // sequential (order matters, or you're rate-limited) for (const u of users) await save(u); Review check: any async callback passed to forEach, filter, or reduce deserves a second look.
2. .sort() without a comparator sorts strings
console.log([10, 9, 1, 100].sort()); // [ 1, 10, 100, 9 ] The default sort converts elements to strings and compares UTF-16 code units. It works on the small test array in the PR description, then breaks on real prices or scores.
Fix:
[10, 9, 1, 100].sort((a, b) => a - b); // [ 1, 9, 10, 100 ] If you need a copy, toSorted((a, b) => a - b) doesn't mutate the original.
Review check: a bare .sort() on anything numeric.
3. map(parseInt) passes the index as the radix
console.log(['1', '2', '3'].map(parseInt)); // [ 1, NaN, NaN ] map calls its callback with (value, index, array). parseInt takes (string, radix), so '2' gets radix 1, which is out of range, and '3' gets radix 2, where 3 isn't a valid digit. Both return NaN.
Fix:
['1', '2', '3'].map(Number); // [ 1, 2, 3 ] ['1', '2', '3'].map((s) => parseInt(s, 10)); // [ 1, 2, 3 ] Review check: point-free callbacks (map(fn)) where fn takes more than one parameter.
4. Spread is a shallow copy
const defaults = { theme: 'dark', flags: { beta: false } }; function withBeta(cfg) { const copy = { ...cfg }; copy.flags.beta = true; return copy; } withBeta(defaults); console.log(defaults.flags.beta); // true (the shared default changed) The spread copies the top level only, so copy.flags is the same object as defaults.flags. Now every user gets the beta flag.
Fix:
const copy = structuredClone(cfg); // or, if you only change one branch: const copy2 = { ...cfg, flags: { ...cfg.flags, beta: true } }; Review check: "copy, then mutate a nested field" anywhere near shared config, state or caches.
Let tools take the first pass
Review is a human job, but you don't have to catch everything by eye. Set up the cheap checks so your attention goes to logic:
- Lint for the known traps. ESLint's
radixrule (also available in Oxc) flagsparseIntcalls without an explicit radix, which would have caught example 3. - Type your data. In TypeScript, a
number[]that's being sorted, or a config object markedReadonly, makes intent visible in the diff. It also gives an agent more to work with when it drafts the next change. - Write one test per bug class. For example 1: assert that the rows exist after
saveAllresolves, not just that it resolved. That single assertion turns a silent bug into a red build. - Give the agent your rules. If your repo has an agent instructions file, add a line like "never use async callbacks in forEach". GitHub's Octoverse data shows new repos with an agent config file logged 8× as many commits as those without one. That's a correlation, not proof, but it's cheap to try.
A 60-second review routine for AI-drafted PRs
- Read the diff, not the description. The summary tells you what was intended. The diff tells you what happened.
- Find the async boundaries. Every
await, callback and promise: is anything floating? - Check the tests assert behaviour. A test that only checks that a function "doesn't throw" proves very little.
- Ask "what's shared?" Config, module-level state, caches, defaults.
- Run it. If you can't run it locally, ask for a reproduction.
None of this is new. It's the same review discipline as always. What's changed is how much code arrives for review, and how convincing it looks.
Wrap-up
AI-assisted coding makes writing code cheaper. It doesn't make knowing why code is wrong any cheaper, and that's the part you're paid for. Practise on the four examples above. Then try spotting them in your own last few PRs.
If you'd rather learn this by building, our YouTube channel walks through full production apps step by step: JavaScript Mastery.
Sources: GitHub Octoverse 2026 · Stack Overflow Developer Survey 2026 results
