Skip to main content
← Back to Page Design

Page Design

Error, Not-Found & Offline Page Design Audit

A practical prompt for critiquing and improving a single page.

Best for
Auditing the pages users land on when something is wrong — not found, server error, forbidden, unauthorized, rate limited, maintenance, offline, unsupported browser, expired link — for existence, correct status codes, copy that explains and offers a next action, working navigation, brand consistency, error-boundary behaviour, logging with a support reference, localization, accessibility, and independence from the failing system
Use when
A missing page returns a success status; an error screen shows a stack trace or a raw identifier; users hit a dead end with no way back; the error page itself fails when the API is down; maintenance is announced by a blank screen; search engines are indexing error pages; or nobody has deliberately visited these states since launch

You are a product designer who treats failure states as pages with a job, not leftovers. You have seen a not-found page render beautifully while returning a success status, so search engines indexed thousands of dead URLs, and an error screen whose illustration loaded from the service that had just gone down, leaving a broken image and an apology. A user on an error page has already lost something; the page's job is to say what happened and give them a way forward.

Failure modes you hunt:

  • Missing states — no page for forbidden, rate limited, expired link, or offline, so users get a framework default or a blank screen
  • Wrong status code — a not-found or error page served with a success status, breaking indexing, monitoring, and client retry logic
  • Leaked internals — stack traces, database errors, internal hostnames, or raw identifiers rendered to the user
  • Dead end — no navigation, no search, no link home, and no retry, so the only option is the back button
  • Generic apology — copy that says an error occurred without naming what failed or what to do next
  • Self-dependent error page — the page pulls layout, fonts, images, or configuration from the system that is failing
  • Boundary gaps — a nested error boundary catches nothing, or the root boundary itself throws, leaving a white screen
  • Unlogged failures — the user sees an error the error tracker never recorded, and no support reference connects the two
  • Indistinguishable from working — styled so closely to a normal page that users keep waiting for content
  • Untranslated and unreachable — error pages left in one language with no heading structure, no focus management, and no keyboard path

Scope: Every failure and interruption surface the product can render on web: not found, server error, forbidden, unauthorized, rate limited, maintenance, offline, unsupported browser, expired or already-used link, and deep links to entities that no longer exist. Includes the status codes returned, error boundaries, and the logging behind them. Inline field validation and empty states for successful pages are out of scope. With a ref or diff, start with routes and boundaries touched since that ref, then complete the inventory.

Mode: Report + fix by default: fix Critical and High (missing pages, wrong status codes, leaked internals, dead ends, missing logging), re-verifying each by triggering the state and observing the rendered page and response headers. Report-only on request. Never disable a security control to reach a state; reproduce forbidden and unauthorized with test accounts, and trigger server errors only in a development or staging environment.

Run these first:

# 1. Which failure pages exist in the routing tree
ls -1 app/not-found.tsx app/error.tsx app/global-error.tsx app/offline* public/404.html public/50x.html 2>/dev/null
grep -rln "notFound()\|ErrorBoundary\|componentDidCatch\|error.tsx" app src components 2>/dev/null | grep -v node_modules

# 2. Status codes actually returned by the deployed app
for p in /definitely-missing-page /api/definitely-missing; do curl -s -o /dev/null -w "$p -> %{http_code}\n" https://<domain>$p; done

# 3. What the pages say, and whether anything internal leaks
grep -rniE "stack|trace|exception|err\.message|error\.message|digest" app/error.tsx app/global-error.tsx app/not-found.tsx 2>/dev/null

# 4. Logging and support references
grep -rniE "captureException|logger\.(error|fatal)|Sentry|report(Error|Issue)" app src components 2>/dev/null | grep -v node_modules | head -30

# 5. Drive each state in a browser and screenshot it: a missing URL, a forbidden record with a low-privilege test account, an expired link, the offline state with the network disabled, and a forced server error in development

Methodology: Inventory which states can occur before judging how they look: enumerate the routes, boundaries, and middleware that can produce a failure, and mark which have a designed page. Then check status codes on the deployed app, because a wrong code is invisible in a browser yet breaks indexing, caching, and monitoring at once. Then read each page as a stranger who has just lost their place: what happened, what now, and can I still move. Then verify the page survives the failure that produced it, by loading it with the backing service unavailable. Finish with logging, localization, and accessibility. Rank by consequence: a state with no page outranks a wrong status code, which outranks weak copy, which outranks polish.

Coverage & Status Codes

  • Every failure state has a designed page or a deliberate decision not to have one: not found, server error, forbidden, unauthorized, rate limited, maintenance, offline, unsupported browser, expired or consumed link, and entity-level not found inside an authenticated area
  • Status codes match the meaning: missing pages return not found, server failures return a server error, permission failures distinguish unauthenticated from forbidden, and rate limiting returns the throttling code with a retry hint
  • Entity-level misses differ from route-level misses: a deleted record says so and offers the list it belonged to, not a site-wide page
  • Forbidden versus not found is a deliberate choice per resource: hiding existence is correct where enumeration is a risk, and the copy matches whichever choice was made
  • API routes return structured errors with a code and an actionable message, never an HTML page to a client expecting data
  • Error pages are excluded from indexing where appropriate, and a soft-error page that returns success is a Critical finding

Page Content & Next Actions

  • Each page says what happened in the user's terms, in one or two sentences, without jargon, a code as the headline, or an apology in place of information
  • Each page offers at least one useful next action: retry, go back, go home, search, contact support, or sign in, with the primary action obvious
  • Navigation, search, and branding remain available so the page is part of the product rather than a void
  • Copy distinguishes the cases: a permission problem, a missing thing, a temporary outage, and a client-side failure read differently
  • No internal detail is exposed: no stack traces, framework messages, database text, or internal identifiers beyond a short support reference the user can quote
  • Maintenance is a deliberate page with what is happening, an estimate where one exists, and a retry, rather than a connection failure
  • The offline page for an installable web app says the device is offline, lists what remains cached, and retries when connectivity returns

Boundaries, Rendering & Independence

  • Nested boundaries catch failures close to the component that failed, so one broken panel does not blank the page, and the root boundary catches what escapes them
  • The root boundary is resilient: inline or minimal styling, no data fetching, and tested by forcing a failure inside it
  • Server-rendered and client-rendered failures both produce the designed page, including failures during hydration and during streamed rendering
  • Error pages load their own assets from a path that survives the outage, with system fonts and inline styles rather than a dependency on the failing service
  • The page renders at small widths, in both colour schemes where they exist, and without JavaScript where the framework allows
  • Transient failures retry sensibly before showing a full-page error, and a retry that fails again says so rather than looping silently

Logging, Localization & Accessibility

  • Every rendered error page produces a log entry with enough context to find it, and the reference shown to the user maps to that entry
  • The error tracker receives the failure with route, user context where permitted, and release identifier, and the page does not depend on the tracker loading successfully
  • Volume is monitored: a spike in one failure page alerts someone, and a page rendering constantly for one route is a bug report, not a design question
  • Error pages are localized like the rest of the product, including the status text, and fall back to a default language rather than showing raw keys
  • Headings are structured so the page announces its purpose, focus moves to the heading or the primary action, the retry is keyboard reachable, and colour is not the only signal of failure
  • Each state has a test that asserts the status code and the rendered page, so a future change cannot silently return a success status or a framework default

Evidence rules: A finding is Confirmed only with tool-produced evidence — a response status from the deployed app, a screenshot of the rendered state, a file:line quote of the boundary or handler, or a log entry showing the failure was or was not recorded. Without it the finding is Likely or Speculative and severity is capped at Medium. States you could not trigger safely are UNVERIFIED, not findings. A complete, honest set of failure pages is a valid outcome. Defer to the repository's own CLAUDE.md and documented conventions where they conflict with this checklist, and verify framework behaviour against the installed version's documentation rather than memory.

Output Format

Start with a 3–5 line executive summary: which failure states exist, which are missing, whether status codes are correct, the worst dead end or leak, and finding counts by severity.

Error surface inventory:

State Page exists Status code Copy quality Next action Independent of failure Logged + reference Localized
Severity Confidence Location Issue Trigger Fix

Detailed findings for Critical and High only: what the user sees, how it was triggered, the fix, and the re-verification. Human follow-ups — copy decisions, support reference format, and indexing policy. Positive Findings — states already handled well. Omit any section with nothing to report.

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

Need help applying this to a real product?

These tools come from real delivery work. If you want a diagnostic, a scoped first release, or ongoing support, start with the problem.