# Debugging

The difference between fixing a bug in 30 seconds and fighting it for an
hour is almost always in how the user reports it. This skill teaches the
capture protocol that gets fast, accurate fixes — and the recovery
techniques for when AI gets stuck.

## When to use this
- The user reports anything is "broken" or "not working".
- The user shows you an error message but no context.
- The user says "the page is blank" or "nothing happens when I click".
- The AI has made the same mistake more than once in a row.

## The capture protocol

Before suggesting any fix, get all five pieces of context. **Do not guess
at the cause** if any are missing — ask the user to provide them.

### 1. The exact error text
- The full error message, copied verbatim, NEVER paraphrased
- The full stack trace if there is one
- Any related console warnings that appeared at the same time

### 2. Where it happened
- The URL or route where the error occurred
- The component or file name if known
- Whether it happens on every visit or only sometimes

### 3. What the user was doing
- The exact action that triggered it (clicked a button, refreshed, etc.)
- Whether the same action used to work and recently broke

### 4. What they expected
- What should have happened instead
- A description or screenshot of the working state if they have one

### 5. What changed recently
- Files edited in the last hour
- Packages installed or removed
- Environment variables added or changed
- Whether they pulled new code from git

## The error report template

Give the user this template to fill in:

```
ERROR: [paste the full error and stack trace]

WHERE: [URL, file, or component name]

WHAT I DID: [the exact action that triggered it]

WHAT I EXPECTED: [what should have happened]

RECENT CHANGES: [edited / installed / changed in the last hour]

TECH STACK: [framework + version, e.g. Next.js 15 App Router, Tailwind v4]
```

## The "blank page" rescue

A blank page is the scariest error because there's no message. The fix sequence:

1. **Open the browser console** (F12 → Console tab). 99% of blank pages have a red error there.
2. **Open the Network tab** and reload. Look for any failed (red) requests.
3. **Check the terminal** running the dev server. Look for compile errors.
4. **Hard refresh** (Cmd+Shift+R / Ctrl+Shift+R) to bypass cache.
5. **Check the URL** — typo? wrong port?

If all five are clean and the page is still blank, the issue is usually a
hydration mismatch or an unhandled promise in a server component.

## The "AI keeps breaking it" rescue

When the AI is making the same mistake repeatedly:

1. **Stop and revert.** Undo all of the AI's changes since the last working state. Use `git stash` or `git checkout .` if needed.
2. **Describe the original problem in one fresh message.** Do not include any of the failed attempts.
3. **Add an explicit constraint.** "Only modify the X component. Do not touch Y or Z."
4. **Ask for the smallest possible fix.** "Just make the button render — we'll add the click handler in a separate step."

Signs you should start a fresh conversation entirely:
- Same wrong answer 3 times in a row (the context is polluted)
- 50+ messages deep (the AI is forgetting earlier context)
- You changed direction mid-conversation
- You manually edited files and the AI is now working from a stale picture

## Common error patterns and their fixes

### `TypeError: Cannot read properties of undefined (reading 'X')`
The code is accessing a property on something that hasn't loaded yet.
**Fix:** Add a null check (`user?.name`), use optional chaining throughout, or add a loading state.

### `Hydration failed because the server rendered HTML didn't match the client`
A server component and client component rendered different content.
**Common causes:**
- `Date.now()`, `Math.random()`, or browser-only APIs in a component that runs on both
- Using `window` or `localStorage` without checking for client side
- Different content based on user locale or timezone
**Fix:** Wrap the dynamic part in `useEffect`, OR use the `suppressHydrationWarning` prop, OR mark the component as `"use client"`.

### `Module not found: Can't resolve 'X'`
The package isn't installed.
**Fix:** `npm install X`. Or the import path is wrong — check capitalization and the exact filename.

### `429 Too Many Requests` / `Rate limit exceeded`
You're hitting an API too fast.
**Fix:** Add debouncing (`use-debounce` package), caching, or exponential backoff.

### `CORS error`
The browser blocked a cross-origin request.
**Fix:** Configure CORS on the server, OR proxy the request through your own API route handler.

### `Unauthorized` / `401`
The auth token is missing or expired.
**Fix:** Ensure the request includes the auth header, OR refresh the token, OR re-sign-in.

### `Maximum update depth exceeded` (React)
A useEffect is updating state that triggers itself in an infinite loop.
**Fix:** Check the useEffect dependency array — you probably have a function or object that gets recreated on every render. Wrap it in `useCallback` or `useMemo`.

### `Cannot find module '@/...'`
Path alias not configured.
**Fix:** Check `tsconfig.json` has `"paths": { "@/*": ["./src/*"] }` and `baseUrl` is set.

### `PrismaClientInitializationError` / Drizzle connection errors
Database connection failing.
**Fix:** Check `DATABASE_URL` env var. In production, ensure connection pooling is configured (Supabase pooler, Neon pooler, or Prisma Accelerate).

### Supabase query returns empty array but rows clearly exist
Row Level Security is silently filtering them out. RLS doesn't error — it
just hides rows the current user can't read, so your query returns `[]`
instead of throwing.
**Fix:** Three things to check, in order:
1. **Is RLS enabled on the table?** Supabase dashboard → Table Editor →
   click table → "Enable RLS" toggle. If disabled, anon key reads everything.
   If enabled, the next checks matter.
2. **Is there a SELECT policy?** Authentication → Policies → click the table.
   Without a policy that returns `true` for the current user, every read
   returns nothing. Write a permissive policy like
   `USING (true)` for public tables, or `USING (auth.uid() = user_id)` for
   user-owned data.
3. **Are you using the right key?** The anon key is restricted by RLS; the
   service role key bypasses it. For server-side reads of public data, the
   anon key + a permissive policy is correct. For admin actions, use the
   service role key (server-only, NEVER in client code).

Quick test: temporarily run the same query in the Supabase SQL editor with
a `SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.sub" = 'user-id';`
to simulate the user. If it returns rows there but not in your app, your
client is using the wrong key.

### Page shows stale data after a database update
ISR is doing exactly what you told it to: serving the cached version.
**Fix:** After any mutation that should update a cached page, call
`revalidatePath` or `revalidateTag` from your server action / route handler:
```ts
import { revalidatePath } from "next/cache";

export async function updateProduct(id: string, data: ProductInput) {
  await supabase.from("products").update(data).eq("id", id);
  revalidatePath("/products");           // listing
  revalidatePath(`/products/${data.slug}`); // detail
  revalidatePath("/");                   // homepage if relevant
}
```
If the page is wrapped in a layout with `revalidate = 1800`, your option
is to either lower the timer (less stale, more rebuilds) OR call
`revalidatePath` explicitly after every mutation. The latter is faster
and cheaper.

If you're sure you called `revalidatePath` and it's STILL stale: check that
the path string matches exactly (no trailing slash, no `?` query string,
case-sensitive). The path matcher is literal, not a glob.

### Service worker is serving stale assets / blank page after deploy
The browser cached the old service worker, which cached old chunks, which
404 because the chunk filenames changed in the new build. Result: blank
page or "ChunkLoadError" in the console.
**Fix:**
1. **Bump the service worker version**. In `public/sw.js`, change a
   `CACHE_VERSION` constant — it triggers a new install on next visit.
2. **Force-update on deploy**. In your service worker:
   ```js
   self.addEventListener("install", (e) => self.skipWaiting());
   self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
   ```
3. **Exclude API routes from caching**. The most common bug: caching
   `/api/*` so users see stale data forever. The fix is in your service
   worker's fetch handler:
   ```js
   self.addEventListener("fetch", (event) => {
     const url = new URL(event.request.url);
     // Never cache API or external auth/AI endpoints
     if (url.pathname.startsWith("/api/")) return;
     if (url.hostname.includes("supabase.co")) return;
     if (url.hostname.includes("openai.com")) return;
     // ... rest of caching logic
   });
   ```
4. **Tell users to hard-reload** when in doubt: Cmd+Shift+R / Ctrl+Shift+R
   bypasses the service worker entirely.

If you're just debugging locally, open DevTools → Application → Service
Workers → "Update on reload" + "Bypass for network". This makes the page
behave as if there's no service worker.

## Browser DevTools mastery

Every vibe coder should know these five DevTools tabs cold:

- **Console** — JavaScript errors and `console.log` output. Filter by Error/Warning level.
- **Network** — every request the page makes. Click any row to see the request, response, and timing. Filter by Fetch/XHR for API calls.
- **Elements** — inspect the DOM and computed CSS. Right-click anything → Inspect. Toggle CSS properties live.
- **Sources** — set breakpoints in your code. Debugger statement (`debugger;`) drops you here.
- **Application** — cookies, localStorage, service workers, indexedDB.

## Console.log strategy

Don't sprinkle `console.log` everywhere. Be deliberate:

```js
// Tag your logs so you can find them
console.log("[checkout]", { user, cart });

// Use console.table for arrays of objects
console.table(users);

// Group related logs
console.group("API call");
console.log("Request:", req);
console.log("Response:", res);
console.groupEnd();

// console.error gets a stack trace
console.error("Failed to fetch user:", err);

// Conditional logging
if (process.env.NODE_ENV !== "production") {
  console.log("debug:", value);
}
```

When the bug is fixed, **remove the logs**. Don't ship them to production.

## When to reach for production debugging tools

For bugs that only appear in production, install **Sentry**:

```bash
npx @sentry/wizard@latest -i nextjs
```

You get:
- Every error with full stack trace
- The user session that triggered it
- Browser, OS, screen size
- A replay of the user's actions before the crash

`console.log` is for local debugging. Sentry is for production debugging.

## What good debugging looks like

- Every error in the console is investigated, not ignored
- The user can describe the error in one sentence using the template
- The fix is targeted to the actual cause, not a guess
- After the fix, you confirm the original action now works AND nothing else broke
- Production errors flow to Sentry, not silently disappear
- You write a one-line commit message that names the bug

## Common mistakes to avoid

- **Guessing the cause.** Always verify with the actual error and stack trace.
- **Trying random fixes.** Each random fix adds noise. Stop and gather context.
- **Ignoring the console.** It almost always has the answer.
- **"It works on my machine."** Get the user's exact error, not your assumption.
- **Fixing the symptom, not the cause.** A try/catch that swallows the error is not a fix — it's a hiding place.
- **Console.log everywhere.** Be deliberate. Tag your logs. Remove them after.
- **Skipping the reproduction step.** If you can't reproduce a bug, you can't fix it. Get steps from the user.
- **Not using git bisect.** When a bug appeared "recently", `git bisect` finds the exact commit that broke it.
- **Asking AI to "just fix it" without context.** AI can't fix what it can't see. Show the error AND the relevant code.

## Going deeper
- Debugging: https://www.codebooks.ai/debugging
- When Things Break: https://www.codebooks.ai/when-things-break
- Error Monitoring: https://www.codebooks.ai/error-monitoring


---

This skill is part of the **CodeBooks Vibe Coding Skills Library**.
Browse all skills, install guides, and the source chapters at
https://www.codebooks.ai/skills
