Reviewing error messages in Next.js App Router
Error messages drift. They start out considered — the team writes
"Something went wrong. Refresh the page to try again." — and six
months later there are 40 copies of "Error occurred" scattered across
toast calls, error.tsx files, and 401 handlers. Nobody meant to
ship that. Nobody reviewed it.
This guide shows what ContentRX catches in the error_recovery
moment across a Next.js 15 App Router app, using three real surfaces:
the global error.tsx boundary, the per-route not-found.tsx, and
inline toast errors.
Setup
Assumed stack:
- Next.js 15 App Router with
error.tsxandnot-found.tsxconventions - A toast library (sonner, react-hot-toast, or similar)
- ContentRX installed via the MCP server or the GitHub Action
The error_recovery moment weighs two standards especially heavily:
VT-05 (empathy scaled to severity) and
CLR-02 (lead with the most important
information). Most error-copy violations are one of those two.
What you'll see — a global error boundary
Here's a typical app/error.tsx before review:
"use client";
export default function Error({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div className="p-8">
<h2>Something went wrong!</h2>
<p>An error occurred.</p>
<button onClick={reset}>Try again</button>
</div>
);
}
ContentRX raises three violations:
Violation 1 — <h2>Something went wrong!</h2>
- GRM-03
[block]— The rule is explicit: "Never use [exclamation points] in error messages or alerts." The enthusiasm reads as dismissive when the user is already frustrated. - VT-05
[warn]— "Something went wrong" is the canonical low-empathy error message. It tells the user nothing and implicitly shrugs. Suggested rewrite: "We couldn't load this page" (takes responsibility, names what broke).
Violation 2 — <p>An error occurred.</p>
- CLR-02
[block]— The rule asks you to lead with the most important information. "An error occurred" has zero information density — it's a placeholder someone forgot to replace. Suggested rewrite: "A temporary issue on our end interrupted the request" or "A network error stopped us from loading this page".
Violation 3 — <button onClick={reset}>Try again</button>
- ACT-02
[info]— "Try again" is moderately vague. Not a hard block in theerror_recoverymoment — retry-style buttons are a well-known pattern — but the rule prefers specific verbs. Suggested alternative: "Reload" (one word, specific action) or leave "Try again" if your team prefers it.
The fix
"use client";
export default function Error({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div className="p-8">
<h2>We couldn't load this page</h2>
<p>A temporary issue on our end interrupted the request.</p>
<button onClick={reset}>Reload</button>
</div>
);
}
Three changes, zero blocking violations. The tone shifts from "the user caused this" to "we caused this and we can fix it" — VT-05's core ask.
Inline toast errors
Toast errors are where this pattern scales. A 20-file codebase
typically has 10–20 toast.error(...) calls. Once you've reviewed
the first few, the same violations repeat:
Before:
toast.error("Failed to save");
toast.error("Something went wrong");
toast.error("Error");
After:
toast.error("Your changes didn't save — try once more.");
toast.error("Couldn't reach the server. Check your connection.");
toast.error("That subscription has already been canceled.");
The after versions are longer — 8–12 words instead of 2–4 — but they tell the user what happened AND what to do. The error recovery moment is specifically calibrated to accept that extra length.
404 / not-found pages
not-found.tsx runs in the same moment and the same rules apply.
A common anti-pattern:
// app/not-found.tsx — BEFORE
export default function NotFound() {
return (
<div>
<h1>404</h1>
<p>Page not found.</p>
<a href="/">Home</a>
</div>
);
}
All three strings flag on CLR-02 (lead with information) and the link flags on ACC-01 ("Home" alone is borderline — works if the surrounding context makes the destination unambiguous).
After:
export default function NotFound() {
return (
<div>
<h1>We couldn't find that page</h1>
<p>The URL may be out of date, or you may have followed a
broken link.</p>
<a href="/">Back to the dashboard</a>
</div>
);
}
Running this on your codebase
Via the MCP server (writing new errors): Claude Code consults ContentRX when you write a new toast or error page. Violations surface before the code lands.
Via the GitHub Action (existing codebase): Scope the action to
error-prone files in a paths: filter:
on:
pull_request:
paths:
- '**/error.tsx'
- '**/not-found.tsx'
- '**/use-toast.ts'
- '**/toast-helpers.ts'
This keeps the action focused on the pages where violations compound and avoids blowing through your quota on every UI tweak.