How to review code an AI wrote

Leonardo Gurgitano5 minLeer en español

A regular, tidy concrete surface photographed head on.
A regular, tidy concrete surface photographed head on.

You asked for a function, it wrote one, you ran it and it works. The code looks tidy: clear names, error handling, even comments.

And that is the problem. Code written by an AI always looks good. It does not carry the signals you learned to spot in a person’s rushed code: variables called temp2, two-hundred-line functions, comments saying “fix this later”. It is tidy even when it is wrong.

Reviewing it is not a matter of reading more carefully. It is a matter of looking at different things. These six, in this order.

1. Does it do exactly what you asked, or something similar?

This is the most common mistake and the easiest to miss, because the result looks a lot like what you wanted.

You asked for “sort the users by registration date”. You get back:

users.sort((a, b) => a.registeredAt - b.registeredAt);

It sorts, yes. Oldest first. If you wanted the most recent first, it is backwards. The function runs, throws no error, and the list comes out sorted. It is just backwards.

The check: read what you asked for again and compare it sentence by sentence with what the code does. Not with what it says it does: with what it does.

2. The edge cases

An AI writes the happy path very well. The edges, less so, because those cases appear less often in what it learned from.

function average(numbers) {
  const sum = numbers.reduce((a, b) => a + b, 0);
  return sum / numbers.length;
}

Correct for [1, 2, 3]. With an empty list it returns NaN, which then travels through your whole system and shows up on the user’s screen as “NaN” without anything having failed along the way.

The four questions always worth asking:

  • What happens if the list is empty?
  • What happens if the value is null or was never sent?
  • What happens if the number is zero, or negative?
  • What happens if the string is empty, or has accents and emoji in it?

3. Libraries that do not exist

This sounds odd until it happens to you. Sometimes the code imports a library with a very reasonable name that simply does not exist, or that exists but does not have that function.

import { formatRelativeDate } from 'date-fns';   // does not exist

It gets caught at install time or at run time, so it rarely gets far. But there is a dangerous version: if the invented name is plausible, somebody may have published a package under that exact name hoping you will install it. It is a known attack.

Before installing something an AI suggested, look at the package: how many downloads it has, when it was last published, whether the repository exists. Ten seconds.

4. Error handling that hides the error

This pattern shows up constantly:

try {
  const data = await fetchData();
  return data;
} catch (error) {
  console.error(error);
  return null;
}

It looks responsible. It is handling the error. But what it does is turn a failure into a null that carries on through your program. Three functions later something will break, and the error message will have nothing to do with the real cause.

The right question at every catch: can this code genuinely continue without that value? If the answer is no, let the failure propagate. A loud error in the right place is worth more than a silent one in the wrong place.

5. What is not in the file you are looking at

An AI sees what you handed it. If you asked for a function in one file, it does not know that another file already has something almost identical, or that your project has an established way of doing things.

The result is silent duplication: two functions validating emails with slightly different rules, two date formats, two ways of calling the API.

Before accepting a new function, search your project for something similar. A grep for the name of the concept is almost always enough.

6. The numbers and strings written by hand

if (user.plan === 'premium' && daysSinceSignup > 30) {

Where did the 30 come from? And is 'premium' exactly the string your database uses, or does it say 'PREMIUM' over there?

When an AI does not know your values, it invents reasonable ones. Every number and every literal string in generated code is something to verify against your actual system.

The order I use

When the code is short, all of this takes a minute. When it is long, an order helps:

  1. Read what I asked for and compare it with what the code does.
  2. Find the literal values and verify each one.
  3. Mentally test with an empty list, null and zero.
  4. Look at every catch and ask whether it can continue without that value.
  5. Check that the libraries exist.
  6. Check whether this was already in the project.

A way to make them show up on their own

Everything above is manual review. There is one thing that makes several of these problems surface without you looking for them: write the test before you ask for the code, even just one.

test('average of an empty list returns 0', () => {
  expect(average([])).toBe(0);
});

That three-line test turns an ambiguous instruction into a verifiable criterion, and catches case 2 without you having to remember to look for it.

What does not change

The old rule still holds: do not ship code you cannot explain. If somebody asks why that line is there and the answer is “the AI gave it to me”, that code is not yours yet — and when it fails at three in the morning, you will not be able to fix it either.

Comments