AI PRs look cleaner than human ones. That's what makes them harder to review.
Hallucinated APIs, plausibly-typed nonsense, stale Angular patterns — the bugs AI ships are not the ones you're trained to catch. My updated code review checklist.
The first time an AI-generated PR burned me, I approved it.
The diff was clean. The types were correct. The tests passed. I did my usual checks, left a couple of style comments, and clicked "Approve." Three days later, in staging, the method it was calling didn't exist in our version of the library. The model had referenced docs from a version two major releases ahead of what we were running. TypeScript didn't catch it. The tests didn't catch it. I didn't catch it.
That was about eight months ago. Since then I've reviewed dozens of AI-generated PRs on my team, and I've learned that the failure modes are genuinely different from what human engineers produce. Not always worse — sometimes better. But different in ways that catch you off guard if you apply the same review habits you built for human code.
The mental model most engineers are working with is wrong
When you review a human PR, you're looking for a specific class of bug: the off-by-one, the missing null check, the forgotten async/await, the copy-paste from the wrong context. You're also checking for intent mismatches — did this person understand the requirements, or did they build the wrong thing well?
Those bugs still appear in AI-generated code. But there's a whole other class that's now more common: the plausible-looking mistake. The kind of bug that looks correct, types correctly, passes your linter, and only fails at runtime or under a specific condition you didn't think to test.
Human engineers make mistakes through gaps in attention or knowledge. AI models make mistakes through pattern-completion. The result looks different, and catching it requires a different eye.
Hallucinated APIs, and why TypeScript doesn't save you
This one stings, because TypeScript is supposed to catch exactly this kind of thing.
The problem is that models are trained on documentation, blog posts, and changelogs from multiple library versions simultaneously. They'll confidently generate code against an API that exists somewhere in that training data, just not in the version you're actually running.
Here's one I caught last month. We're on Angular 18 with HttpClient. The AI-generated service included an option I didn't recognise:
// AI-generated — looked fine, failed silently at runtime
getUser(id: string): Observable<User> {
return this.http.get<User>(`/api/users/${id}`, {
observe: 'body',
// transferCache is real but scoped to SSR transfer state —
// using it here does nothing, and passes no error
transferCache: { includePostRequests: true }
});
}
transferCache is a real option — it exists in HttpTransferCacheOptions for SSR scenarios. The model had seen it in Angular docs and applied it here without understanding the context. Passing an unrecognised option to http.get() doesn't throw; it's silently ignored. TypeScript was happy because HttpOptions is broad enough to accept it without complaint.
The fix took 30 seconds. The problem is I nearly didn't spot it.
What I check now: For any method, option, or configuration I don't immediately recognise, I go to the actual source — the framework's GitHub or official docs, not AI-summarised Stack Overflow. It adds maybe 90 seconds per review. It's caught three issues like this in two months.
Plausibly-typed nonsense
This is the category that worries me most when junior engineers are reviewing AI output.
The types are correct. The logic compiles. The code is wrong.
// AI-generated retry logic — types perfectly, semantically broken
updateUserPreferences(prefs: UserPreferences): Observable<void> {
return this.http.put<void>(`/api/users/${prefs.userId}/preferences`, prefs).pipe(
// this looks like good defensive practice
// it is not, for a non-idempotent PUT
retry({ count: 3, delay: 1000 }),
catchError(this.handleError)
);
}
That retry looks reasonable. Adding resilience to network calls is good practice, right? Except this is a PUT to a non-idempotent endpoint. If the request succeeds server-side but the response is lost in transit, we retry and write the preference update three more times. On our backend, that caused a duplicate audit log entry that our compliance team flagged two weeks after the PR merged.
The AI doesn't know what your endpoint does. It sees an HTTP call and applies a general best-practice pattern. The type system sees a correctly-typed pipe() chain and has nothing to object to.
I see the same pattern with catchError handlers that swallow errors or re-throw them in the wrong shape because the model doesn't know your application's error contract. It generates the shape of error handling without understanding the substance of it.
What I check now: For any error handling, retry logic, or async coordination the AI added, I ask: is this semantically correct for what this specific endpoint actually does? Not "does this typecheck" but "is this right for this context."
Stale patterns from training data
Models have a training cutoff, and the majority of their training data skews toward older, more prevalent code. Angular 14 has years of Stack Overflow answers, tutorials, and GitHub repos behind it. Angular 20 has months.
The result: AI-generated Angular code often uses patterns that are technically valid but not idiomatic for the version you're on.
// What AI keeps generating — valid but firmly Angular 14-era
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean | UrlTree {
return this.authService.isLoggedIn() || this.router.parseUrl('/login');
}
}
// What the codebase actually uses — functional guards, Angular 17+
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isLoggedIn() || router.parseUrl('/login');
};
The class-based guard compiles and runs. But it's against the direction Angular has been moving for two years, it doesn't compose cleanly with standalone components, and it means the next engineer maintaining this file has to context-switch mid-read.
I see the same thing with NgModules appearing in Angular 17+ codebases, ComponentFactoryResolver long after it was deprecated, and RxJS 6 .pipe(map(...)) patterns in projects running RxJS 7 with the newer operator signatures. Each one works. All of them leave the codebase a little less coherent.
What I check now: When AI generates any structural pattern — guards, interceptors, resolvers, providers — I check whether it matches what the rest of the codebase uses, not just whether it's valid TypeScript.
Context blindness: the bug that doesn't show in isolation
This one is harder to articulate but consistently true: AI has no knowledge of your codebase's conventions, your team's agreed patterns, or the architectural decisions you made six months ago that aren't written down anywhere obvious.
We have a BaseComponent that handles subscription teardown using a consistent pattern. Every component in our Nx monorepo extends it. AI-generated components don't — they use takeUntilDestroyed() or manual ngOnDestroy(), which works but creates inconsistency that costs time to untangle when you need to change the teardown behaviour globally.
We have a central error handling service that all HTTP errors route through. AI-generated services sometimes implement their own catchError logic, creating parallel error handling paths that diverge silently when we update the global behaviour.
None of these are type errors. None would show up if you're reviewing quickly. They're the slow-burn technical debt that AI can generate faster than humans because it's generating patterns it's seen elsewhere, not the patterns specific to your system.
The honest counterpoint
AI-generated PRs are also, in my experience, often better than human ones in ways that matter.
The variable naming is more consistent. There's less copy-paste drift between similar functions. Comments tend to describe what the code does rather than restating the syntax. The overall structure is cleaner because the model isn't tired or distracted or under delivery pressure.
And the defect rate on features that do work correctly is, in my experience, roughly equivalent to careful human output. I'm not arguing AI code is worse. I'm arguing the specific failure modes are less familiar, so the review instincts you've spent years developing don't fire on them as reliably.
That's the actual problem.
What I actually do now
My review process has changed in three concrete ways.
For any non-trivial API usage I don't immediately recognise, I verify against the version pinned in package.json — not against search results or AI-summarised docs. This catches the version-mismatch hallucinations before they hit staging.
For any error handling, retry logic, caching, or async coordination, I ask whether it's correct for this specific operation, not just whether it's a reasonable general pattern. The AI has good general knowledge and no specific knowledge. That gap is where it fails.
For structural patterns — guards, interceptors, services, base classes — I check whether they match the conventions already established in the codebase. AI-generated code is often correct in isolation and inconsistent in context.
The total review time is similar. I've just shifted effort away from "is this syntactically right" (the AI rarely gets that wrong) toward "is this semantically correct and contextually consistent" (which is exactly where it goes wrong most often).
The question isn't whether AI-generated PRs are good or bad. It's whether your review process was designed to catch the specific ways they fail. Mine wasn't — until a few of them slipped through and taught me what to look for.
Build the checklist before you need it.
Building full-stack applications with AI-assisted workflows in Germany. I occasionally take on senior engineering engagements — book a call if you want to talk.
Deepak
Full-Stack Engineer based in Germany. I write about performance, Angular, Node.js, design systems, and the craft of building exceptional web experiences.
Book a 15-min call →