Skip to content

v0.92.0 — Error messaging: the standard and the helper

Shipped 2026-09-19 — merged to main as 66fb9ae2, tagged v0.92.0, pushed. Built on version/0.92.0, thirteen commits for eleven tasks. The third of the fifteen releases ROADMAP.md sequences from the August 2026 heuristic evaluation; the roadmap page is error-messaging-standard, part one of the error messaging epic. The page for users is Error Messages.

The rule: every error a user sees is a system fault or a user error, and anything not explicitly the second is the first. A system fault says one sentence — Something went wrong on our side. — and offers Retry, Reload and Report; a user error says the specific thing our code can say, and the fix when there is one. A stack trace presented as though the user did something wrong was the failure the evaluation named, and the default is what ends it.

The type and the wire

  • AppError (E.1), in @purple-cat/shared: kind, code, message, detail, retryable, status, entity. fromUnknown classifies anything thrown or received and never throws: a coded 4xx is the user's; a 5xx, a network failure, a failed chunk, a dead backend are retryable faults; a TypeError is a fault a retry will not cure; everything else a fault.
  • The server speaks it (E.2). Every error body now carries kind and code — { error: "Map not found", kind: "user", code: "not_found" } — stamped by one middleware from the status when the route said nothing more, so all ~850 existing 4xx sites gained a code without changing. An uncaught throw is a 500 with the envelope and no stack. error stays a string, on purpose: the page's nested shape would have broken every reader to gain nothing.

Reporting

  • reportProblem (E.3), the one helper behind every Report button: builds the debug bundle — route, build sha, user id, entity ids, the classified error, the last twenty request summaries (never a body), the console tail (redacted), viewport — and hands it to a transport that can be swapped without touching a surface. Sends on click, never on its own.
  • POST /api/report (E.4): no database, no auth round-trip, size-capped and rate-limited, so it answers while the rest of the API cannot; email to support when SMTP is there, the server log when not; and when even that fails, the client opens a mailto: with the bundle's text. One of the two always happens.

The surfaces

  • SystemFaultPanel (E.5), behind ErrorBoundary, ErrorPage and the query client's global onError (as a notification, once per fault kind per ten seconds): the sentence, Retry when possible, Reload, Report, the detail folded away, Status when configured, Support.
  • UserErrorPanel (E.6) and showError: the message and the fix — the surface's own, or what the code implies; permission denials say contact the owner(s) of this asset until roles can name someone.
  • Backend-down detection (E.7): one flag, one probe. While the server is unreachable every API fetch fails at once with the one fault, the app re-probes every fifteen seconds, and — found on the way — re-opens a socket that Socket.IO had given up on, which used to stay dead for the session after any outage over fifteen seconds.
  • Validation is a user error (E.8): the sheet's issue alerts render the shared body; the Create Token chooser's flows say what is missing instead of returning silently; a slug collision is an AppError whose fix is the chips.

The sweep and the guard

  • Three sweeps from the source outward (E.9): 189 failed-fetch throws now carry the status and the body's code; 51 error notifications classify before they render; 97 inline error strings classify before they are set.
  • The guard (E.10) fails on the three ways that erodes, and on its first run found nine sites the scripts had missed.

Left for part two

Part two of the epic (v0.100.0) adds the productive actions — request access, name who can grant it, offer a rollback — once the things behind those buttons exist. The status link waits on a status URL, which is a decision rather than code.