6 min read

Reviewing AI-Generated Code Like It Came From a Junior

The junior-developer framing is a good default that breaks in three specific ways: the errors don't cluster, the confidence is flat, and nothing is learned. Those three differences tell you exactly where to spend review attention.

AICode ReviewAndroidEngineering LeadershipCode Quality
HR

Hessam Rastegari

Senior Android Developer · 12 years shipping Android

TL;DR — Reviewing generated code like a junior's PR is the right instinct, but three differences change the method. A junior's mistakes cluster (so you can sample); a model's don't. A junior signals uncertainty; a model's confidence is flat. A junior learns from review; a model starts tomorrow with nothing. So: read the whole diff, lead with structure and error paths, grep for duplication, verify APIs exist, and scrutinise the tests hardest. The author remains accountable — always.

Where the analogy holds

Genuinely, and worth keeping: you read it properly, you don't merge what you don't understand, you check it against the codebase's conventions, and you assume competence without assuming correctness. That's already better than the two failure modes at either extreme — rubber-stamping because it looks polished, or rejecting on principle.

Then it breaks in three places.

1. The errors don't cluster

A junior who doesn't understand structured concurrency gets it wrong consistently. That's a gift: find one instance, and you know both the root cause and where the rest are. Review scales, because error distribution is lumpy and explainable.

Generated code is correct in nineteen places and wrong in the twentieth, and the twentieth doesn't share a cause with anything. It might be a subtly wrong boundary condition in the middle of otherwise sound logic.

The practical consequence is the important part: you cannot sample generated code. With a junior, skimming a third of a large diff carefully tells you something about the other two thirds. Here it tells you about that third. If the diff is too big to read completely, it is too big to merge — which is an argument for constraining generation to reviewable-sized chunks in the first place.

2. Confidence is flat

"I wasn't sure about this part, can you check it?" is the highest-value sentence in code review. It directs attention with information the reviewer doesn't have.

You never get it. Code the model has effectively memorised from a thousand examples and code it extrapolated into existence look identical — same fluency, same clean structure, same confident naming. The uncertainty signal that reviewers rely on is simply absent, so you have to supply it yourself from the shape of the problem: which parts of this were unusual, project-specific, or unlikely to appear in public code? That's where to look.

3. Nothing is learned

Review of a junior's work is an investment: explain the convention once, and the next twenty PRs comply. Explain it to a model and the correction survives the session.

Two consequences. First, the return on teaching in the review comment is zero, so put that energy into the durable layer instead — the conventions file, a lint rule, a check in CI (the same argument as in the conventions post). Second, and more subtly: review fatigue is real, and it's worse when there is no compounding payoff. Volume of generated code is exactly the thing that makes the review harder while removing the reward that normally sustains it.

What to actually check, in order

1. Shape. Should this code exist at all, in this layer, in this module? Generated code answers the question you asked with impressive fidelity — including when the question was wrong. A ViewModel with its own retry loop and cache is a fine answer to "add retry with caching" and a bad answer to the problem, which was that the repository should have handled both.

2. Error paths. The happy path is almost always right. Spend your attention on:

  • Cancellation — does it survive a catch (e: Exception)? (It usually doesn't.)
  • Timeouts and retries — is there a bound, or an unbounded retry loop?
  • Empty and single-element cases.
  • Partial failure — three calls, one fails.
  • Anything swallowed: catch { }, ?: return, runCatching { }.getOrNull().

That last pattern is the most common defect I see in generated Kotlin. It makes the code compile and the tests pass while erasing the failure.

3. Duplication. It cannot see the DateFormatter you already have in core:common, so it writes a second one. Grep for the concept, not the name — the duplicate will have a different name.

4. API existence. Plausible-looking methods that don't exist, or exist with different semantics. The compiler catches most of this in Kotlin, which is why the survivors are dangerous: a real method whose behaviour differs from the assumption (first() vs firstOrNull(), map on a Flow vs a Sequence, a suspend variant that doesn't cancel).

5. The tests, hardest of all. Generated tests describe what the code does, not what it should do. If the implementation is wrong, the test encodes the bug and passes — and now the bug has credentials.

// Passes. Tests nothing.
@Test fun `returns empty list on error`() {
    every { api.fetch() } throws IOException()
    assertEquals(emptyList(), repository.load())
}

Should it return an empty list on error, or an error state? The test doesn't ask, and neither will anyone reading it in six months. Read each assertion and ask whether it states a requirement.

The team rule

The author is accountable for every line they submit, regardless of who or what typed it.

"The AI wrote it" is not a response to a review comment. It's the same standard as copying from Stack Overflow — a submitted diff is a claim that you understand it and believe it's correct. Say this explicitly, because the alternative arrives quietly: PRs get larger, understanding gets thinner, and the first incident reveals that nobody on the team can explain the code.

A few practices that support it:

  • Cap PR size. Reviewability, not generation speed, is the constraint.
  • Have the author state what they verified — not "AI-assisted", which tells the reviewer nothing.
  • Notice if review is becoming a bottleneck, and fix it by generating less, not by reviewing less.

The rule

Review it like a junior's work, with three corrections: read all of it, trust none of the confidence, and put the teaching into CI instead of the comment thread. The analogy's failures are precisely the map of where the bugs are.

Keep reading