ERROR MESSAGE CATEGORIZATION
error-message-categorization.tsx
Categorize errors at the UI boundary (network / auth / validation / server / quota / unknown) before choosing copy, so users see actionable messages, not raw internals.
StarkWHAT THIS PATTERN TEACHES
Why the copy you show must be a function of the error CATEGORY, never of where the catch happened. How to classify on two signals — HTTP status AND error shape (machine-readable code, Retry-After) — and map each category to honest, actionable copy. Unknown errors fall back to a safe generic category.
WHEN TO USE THIS
Any UI that surfaces backend errors. Especially flows where a quota/billing failure could be miscategorized as the user's input fault — a 402 caught in an upload flow must not render 'try a different file'.
AT A GLANCE
export function classify(err: NormalizedError): ErrorCategory {
if (err.isNetworkError) return 'network'
if (QUOTA_CODES.has((err.code ?? '').toLowerCase())) return 'quota'
switch (err.status) { case 402: return 'quota'; case 401: return 'auth'; /* ... */ }
}FRAMEWORK IMPLEMENTATIONS
TypeScript
'use client';
// The exhaustive set of categories the UI knows how to talk about. Keep this
// closed — the COPY map is keyed by this union, so a missing entry is a compile error.
export type ErrorCategory =
| 'quota' | 'rate-limit' | 'timeout' | 'network'
| 'validation' | 'auth' | 'forbidden' | 'not-found' | 'server' | 'unknown';
// Real catch blocks receive heterogeneous junk; normalize at the boundary.
export interface NormalizedError {
status?: number;
code?: string; // machine-readable code from the body ({ code: 'quota_exceeded' })
message?: string; // last-resort signal, never copy
isNetworkError?: boolean;
isTimeout?: boolean;
fieldErrors?: Record<string, string[]>;
}
const QUOTA_CODES = new Set([
'quota_exceeded', 'insufficient_quota', 'plan_limit_reached',