DEV Community

JSGuruJobs
JSGuruJobs

Posted on

5 JavaScript Code Review Patterns Prevent AI Bugs Before They Steal Your Evenings

AI is fast at producing plausible JavaScript. It is bad at the production details that make code safe, fast, and maintainable. Here are 5 review patterns I use to turn AI generated React and Node.js code into something I would actually ship.

Parallelize independent async work

AI regularly writes sequential awaits because they are easy to predict. That code works, but it quietly multiplies latency in every request path.

Before

export async function getDashboard(userId: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
  })

  const projects = await db.project.findMany({
    where: { userId },
    orderBy: { updatedAt: "desc" },
  })

  const notifications = await db.notification.findMany({
    where: { userId, read: false },
  })

  return { user, projects, notifications }
}
Enter fullscreen mode Exit fullscreen mode

After

export async function getDashboard(userId: string) {
  const [user, projects, notifications] = await Promise.all([
    db.user.findUnique({
      where: { id: userId },
    }),
    db.project.findMany({
      where: { userId },
      orderBy: { updatedAt: "desc" },
    }),
    db.notification.findMany({
      where: { userId, read: false },
    }),
  ])

  return { user, projects, notifications }
}
Enter fullscreen mode Exit fullscreen mode

If the three queries each take 40 ms, the first version is roughly 120 ms plus overhead. The second version is closer to 40 ms plus overhead. AI usually optimizes for correctness first. Your review has to add concurrency where the calls are independent.

Cancel stale React requests

This is one of the most common AI mistakes in frontend code. The component fetches correctly, then updates state after unmount, or overwrites fresh data with stale data from an earlier request.

Before

import { useEffect, useState } from "react"

export function UserCard({ userId }: { userId: string }) {
  const [user, setUser] = useState<any>(null)

  useEffect(() => {
    async function load() {
      const res = await fetch(`/api/users/${userId}`)
      const data = await res.json()
      setUser(data)
    }

    load()
  }, [userId])

  if (!user) return <div>Loading...</div>
  return <div>{user.name}</div>
}
Enter fullscreen mode Exit fullscreen mode

After

import { useEffect, useState } from "react"

type User = {
  id: string
  name: string
}

export function UserCard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const controller = new AbortController()

    async function load() {
      try {
        setError(null)
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        })

        if (!res.ok) {
          throw new Error(`Request failed with ${res.status}`)
        }

        const data: User = await res.json()
        setUser(data)
      } catch (err) {
        if (err instanceof DOMException && err.name === "AbortError") return
        setError("Could not load user")
      }
    }

    load()

    return () => controller.abort()
  }, [userId])

  if (error) return <div>{error}</div>
  if (!user) return <div>Loading...</div>
  return <div>{user.name}</div>
}
Enter fullscreen mode Exit fullscreen mode

The AI version passes a happy path demo. The reviewed version survives rapid route changes, unmounts cleanly, and does not leak warnings into the console. This is the same kind of defensive thinking you need when doing debugging JavaScript like a detective in real production code.

Validate unknown input at the boundary

AI loves to trust request bodies. That is how you end up with type coercion bugs, silent bad data, and fragile handlers that explode in production.

Before

export async function createUser(req: Request) {
  const body = await req.json()

  if (!body.email || !body.age) {
    return Response.json({ error: "Missing fields" }, { status: 400 })
  }

  const user = await db.user.create({
    data: {
      email: body.email,
      age: body.age,
    },
  })

  return Response.json(user, { status: 201 })
}
Enter fullscreen mode Exit fullscreen mode

After

import { z } from "zod"

const CreateUserSchema = z.object({
  email: z.email(),
  age: z.number().int().min(13).max(120),
})

export async function createUser(req: Request) {
  const raw = await req.json()
  const parsed = CreateUserSchema.safeParse(raw)

  if (!parsed.success) {
    return Response.json(
      { error: "Invalid request body", issues: parsed.error.issues },
      { status: 400 }
    )
  }

  const user = await db.user.create({
    data: parsed.data,
  })

  return Response.json(user, { status: 201 })
}
Enter fullscreen mode Exit fullscreen mode

Boundary validation removes a whole class of bugs before business logic runs. It also makes your types honest. AI generated code often looks typed because the file is TypeScript. That means nothing if the runtime input is still unchecked.

Replace catch-all error handling with explicit failure paths

AI often writes error handling that hides the real failure and makes incident response worse. Broad try/catch blocks feel safe, but they flatten useful context.

Before

export async function updateSubscription(userId: string, planId: string) {
  try {
    const user = await db.user.findUnique({ where: { id: userId } })
    const plan = await billing.getPlan(planId)

    await billing.assignPlan(user!.billingCustomerId, plan.id)

    return { ok: true }
  } catch {
    return { ok: false, error: "Something went wrong" }
  }
}
Enter fullscreen mode Exit fullscreen mode

After

class NotFoundError extends Error {}
class BillingError extends Error {}

export async function updateSubscription(userId: string, planId: string) {
  const user = await db.user.findUnique({ where: { id: userId } })
  if (!user) throw new NotFoundError("User not found")

  const plan = await billing.getPlan(planId)
  if (!plan) throw new NotFoundError("Plan not found")

  try {
    await billing.assignPlan(user.billingCustomerId, plan.id)
    return { ok: true }
  } catch (error) {
    throw new BillingError(
      `Failed to assign plan ${plan.id} to customer ${user.billingCustomerId}`
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

This version gives you actionable errors and avoids the non null assertion that AI slipped in with user!. Good review is not just about preventing crashes. It is about making failures diagnosable in under five minutes.

Add timeouts and parameterization to every external boundary

AI generated server code often assumes dependencies always respond and user input is always safe. In production, both assumptions are wrong.

Before

export async function searchUsers(term: string) {
  const sql = `
    SELECT id, name, email
    FROM users
    WHERE name ILIKE '%${term}%'
  `

  const result = await db.$queryRawUnsafe(sql)
  const crmProfile = await fetch(`https://crm.internal/search?q=${term}`).then((r) =>
    r.json()
  )

  return { result, crmProfile }
}
Enter fullscreen mode Exit fullscreen mode

After

function fetchWithTimeout(input: string, ms: number) {
  const controller = new AbortController()
  const timeoutId = setTimeout(() => controller.abort(), ms)

  return fetch(input, { signal: controller.signal }).finally(() => {
    clearTimeout(timeoutId)
  })
}

export async function searchUsers(term: string) {
  const result = await db.$queryRaw<
    Array<{ id: string; name: string; email: string }>
  >`
    SELECT id, name, email
    FROM users
    WHERE name ILIKE ${"%" + term + "%"}
  `

  const crmResponse = await fetchWithTimeout(
    `https://crm.internal/search?q=${encodeURIComponent(term)}`,
    3000
  )

  if (!crmResponse.ok) {
    throw new Error(`CRM request failed with ${crmResponse.status}`)
  }

  const crmProfile = await crmResponse.json()

  return { result, crmProfile }
}
Enter fullscreen mode Exit fullscreen mode

The first version risks SQL injection and unbounded waiting on a dependency. The second one is boring in the best way. Boring code is what keeps you out of evening incident calls.

AI is useful when you treat it like a fast junior pair programmer, not an autonomous engineer. Review for concurrency, lifecycle safety, input validation, error specificity, and external boundary hardening. That is where the fake productivity disappears and the real time savings start.

Top comments (0)