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 error-recovery copy 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.tsx and not-found.tsx conventions
  • A toast library (sonner, react-hot-toast, or similar)
  • ContentRX installed via the MCP server or the GitHub Action

The error-recovery context weighs two qualities especially heavily: empathy scaled to severity, and leading with the most important information. Most error-copy findings come back to 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 findings:

Finding 1 — <h2>Something went wrong!</h2>

  • Mechanics [block] — exclamation points read as dismissive in error copy when the user is already frustrated. The voice guide is explicit: leave exclamation points out of error messages and alerts.
  • Voice & tone [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).

Finding 2 — <p>An error occurred.</p>

  • Structure [block] — leads with the wrong thing. "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".

Finding 3 — <button onClick={reset}>Try again</button>

  • Voice & tone [info] — "Try again" is a moderately vague verb. Not a hard block here — retry-style buttons are a well-known pattern — but more specific verbs read better. 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 findings. The tone shifts from "the user caused this" to "we caused this and we can fix it" — exactly what error-recovery copy needs.

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 findings 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. Error-recovery copy is calibrated to accept that extra length when it's load-bearing.

404 / not-found pages

not-found.tsx runs in the same context 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 structure (leading with the wrong thing) and the link flags on accessibility ("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. Findings 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 findings compound and avoids blowing through your quota on every UI tweak.

Categories on the public envelope

ContentRX returns each finding under a customer-facing category — Voice & tone, Mechanics, Structure, Accessibility, Inclusion, Big picture. The categories above match what shows up in your editor diagnostics, your PR comment, and the dashboard.