An inherited AI-written Next.js app usually fails in layers. The browser shows a hydration warning, the terminal shows a server exception, and the last agent changed six files while chasing the first symptom.
Debugging gets faster when you stop asking for a broad fix and start isolating one request path. This runbook targets the App Router and follows the current Next.js debugging guidance for browser and server-side inspection.
Kill the default approach: broad rewrites
The expensive default is to tell another agent, "fix the app." It reads the visible error, replaces a component, updates dependencies, and may remove the symptom without identifying the fault.
Freeze the code first. Reproduce one failure. Trace which runtime owns it. Change the smallest responsible boundary.
By the end you will have
- A deterministic reproduction command
- A client, server, build, or data classification
- A request trace from route entry to failure
- The Next.js Fault Isolation Card
- A regression check that fails before the repair and passes after it
Capture the failure without editing
Record the exact URL, action, input, browser, and environment. Save the first useful stack trace from both the browser console and terminal.
Run the narrowest reproduction:
pnpm dev
# Open the exact failing route and repeat one action.If the bug appears only in production mode, test the production artifact locally:
pnpm build
pnpm startDevelopment and production differ in compilation, caching, source maps, and error presentation. "Works in dev" is evidence about one runtime, not the deployed one.
Commit or save the current diff before debugging. Without a stable baseline, every experiment changes the crime scene.
Classify the runtime before reading code
Use the first observable boundary:
| Evidence | Start here |
|---|---|
| Hydration warning, click handler, browser-only API | Client component |
| Server Component exception, route handler, server action | Node or edge server |
Failure during next build | Compilation, static generation, or configuration |
| Correct request, wrong or missing record | Data access and authorization |
| Works locally, fails after deploy | Environment, hostname, region, or platform integration |
Check "use client" boundaries. A Server Component cannot use browser state or event handlers. A Client Component can receive serializable data from the server, but moving the whole page client-side to silence an error usually creates a larger bundle and hides the architectural mistake.
Trace one request end to end
For a failing form, write the path on paper:
button
-> client submit handler
-> server action or route handler
-> input parser
-> identity check
-> database or provider
-> response mapping
-> success/error UIPlace temporary, structured logs at boundary crossings. Log a request ID and state name, not secrets or the full form body.
console.info("contact.submit", {
requestId,
stage: "validated",
hasEmail: Boolean(input.email),
});Remove noisy debug logs after the fault is understood or convert useful ones into deliberate observability.
You should see: the last successful boundary and the first failed boundary for one request ID.
Read hydration errors as a server-client diff
Hydration fails when the server-rendered tree does not match the browser's first render. Common causes include:
- Reading
window, storage, or viewport state during initial render - Rendering
Date.now(), random values, or locale-dependent output on both sides - Invalid HTML nesting
- Browser extensions injecting markup or providers
- Conditional trees that depend on client-only state
Reduce the component until server and client produce the same first tree. Move browser-only work into an effect or a deliberately client-only leaf. Do not add suppressHydrationWarning unless the mismatch is intentional and contained.
When an error names ethereum, solana, or another injected provider, repeat it in a clean browser profile. Wallet extensions can conflict while defining globals. That test distinguishes application code from extension injection before you rewrite the wallet layer.
Debug actions and handlers at the boundary
Inspect four things in order:
- Parsed input
- Server-derived identity
- Dependency result
- Returned error contract
Use a schema that rejects unknown or oversized input. Confirm the action is running in the expected runtime and has access to required environment variables. A variable available in the shell may be absent from Vercel, assigned only to Preview, or require a redeployment after it was added.
Do not return provider errors directly to the browser. Map them to a stable application error and keep the detailed cause in protected logs.
Separate caching bugs from stale assumptions
Name the expected freshness first:
- Should this fetch be reused across requests?
- Should the route render at build time or request time?
- Which mutation invalidates the data?
- Is the stale value in Next.js, the browser, a CDN, or the database?
Instrument the data source and timestamp. If the database returns the new value but the page does not, follow the cache path. If the database still returns the old value, changing React code will not help.
Avoid adding no-store everywhere. It can make stale data disappear by disabling useful caching across the application.
Build the smallest regression proof
The best regression test targets the failed boundary:
- Pure parsing bug: unit test
- Route handler contract: request-level test
- Server action plus database policy: integration test
- Hydration or navigation failure: browser test
- Production environment mismatch: deployment smoke script
Start with the test failing for the original reason. A green test written after the fix may never exercise the fault.
Magnet: Next.js Fault Isolation Card
# Next.js Fault Isolation Card
REPRODUCE
URL/action:
Input/account:
Dev, production build, or deployed only:
First browser error:
First server error:
CLASSIFY
[ ] Client render or hydration
[ ] Server Component/action/handler
[ ] Build or static generation
[ ] Data/auth boundary
[ ] Environment/platform
TRACE
Last successful boundary:
First failed boundary:
Request ID or deterministic command:
REPAIR
Smallest responsible file:
Unrelated changes excluded:
Failing regression proof:
Same proof after fix:
VERIFY
[ ] Production build
[ ] Exact user path
[ ] Clean browser profile when extensions may inject globals
[ ] Logs contain no secrets or payloadsYou should see: one classified fault, one responsible boundary, and one proof that changes from red to green.
Failure modes
| Debugging move | Why it fails | Better move |
|---|---|---|
| Upgrade every package | Changes the runtime while investigating it | Reproduce on the locked dependency graph |
| Convert the page to a Client Component | Moves the boundary instead of finding the fault | Isolate the browser-only leaf |
Add no-store globally | Hides stale-data causes and raises load | Identify the cache owner and invalidation event |
| Trust one stack trace | Client and server may report different layers | Correlate the same request across both |
| Ask an agent for a broad rewrite | Removes evidence and expands scope | Give it the reproduction and file boundary |
When not to use this runbook
- The failure is reduced to a dependency regression with a confirmed upstream fix
- Production is down and a known-good rollback is safer than live diagnosis
- The project is disposable and rebuilding costs less than understanding inherited behavior
For production data, authentication, billing, or repeated incidents, preserve the trace and regression proof even after a rollback.
Keep a short fault receipt
After the repair, record the symptom, root cause, proof command, and rule that prevents recurrence. If the same category appears twice, add a repository instruction, test, or lint rule. Do not turn every one-off mistake into permanent prompt text.
path
Need the inherited app stabilized?
Maintainer and Advisory packages cover fault isolation, production repairs, verification, and a handoff your team can keep using.
Your next action: fill the Next.js Fault Isolation Card before changing another file, then make one boundary move from red to green.