Error Explainer

Turns a raw error code into what happened, the likely cause and one next step, and warns when a timed-out payment may already have gone through.

StopsPaid twice after a payment timeout error

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/error-explainer.json

The files land in your project. They're yours to edit.

Usage

error-explainer-usage.tsx
import { ErrorExplainer } from "@/components/ui/error-explainer"

<ErrorExplainer
  key="payment"
  code={504}
  explanation={{ title: "We didn't hear back about your payment of ₹2,499" }}
  message="Gateway Timeout"
  details={stack}
  referenceId="PAY-7Q4K-2M9X"
  occurredAt={occurredAt}
  idempotent={false}
  onCheckStatus={() => setNote("Checked: the payment went through. Order #48213 is confirmed, nothing to retry.")}
  onRetry={() => Promise.reject(new Error("still timing out"))}
  onContact={() => setNote("Opening support with the reference attached…")}
/>

What it catches

  • Raw codes with no explanation, mapped through your catalog
  • Retrying before Retry-After allows
  • Retrying a non-idempotent request after an ambiguous timeout (offers Check status first)
  • Errors that keep failing, with a contact option after maxRetries

Props

ErrorExplainer props
PropTypeDefault
code* (required)

Error code: an HTTP status, "network", "timeout" or your own key.

string | number—
message

The raw message from the system, shown under Technical details.

string—
explanation

Overrides for the catalog entry (title, meaning, cause, action...).

Partial<ErrorExplanation>—
catalog

Your own catalog of codes.

Record<string, ErrorExplanation>—
referenceId

Request or trace ID support can search for.

string—
occurredAt

When it failed.

Date | number | string—
details

Stack trace or response body, hidden until asked for.

string—
onRetry

Retries the failed action. May return a Promise; a rejection counts as another failure.

() => void | Promise<unknown>—
retryAfter

Seconds to wait before retrying (from a Retry-After header).

number0
idempotent

False when repeating the action could do it twice (a payment, a send). Ambiguous failures then say to check first.

booleantrue
onCheckStatus

Looks up whether the action actually went through, offered first when a retry might duplicate it.

() => void—
onFix

Runs the "fix" action (sign in, open settings...).

() => void—
onContact

Opens support.

() => void—
maxRetries

Failed retries after which support is suggested instead.

number3
causeLabel

Templates: `{seconds}` and `{count}` are filled in.

string"Likely cause:"
retryLabel

Retry button text.

string"Try again"
retryAnywayLabel

Text for retrying despite an ambiguous outcome.

string"Retry anyway"
waitLabel

Text shown while waiting for Retry-After.

string"Try again in {seconds}s"
checkLabel

Text for the check-status button.

string"Check status"
contactLabel

Text for the contact-support button.

string"Contact support"
ambiguousText

Warning that the request may already have gone through.

string"This may have gone through before the error. Check first…
stillFailingText

Shown after repeated failures.

string"Still failing after {count} tries."
referenceLabel

Label for the support reference.

string"Reference"
copyLabel

Text for the copy-reference button.

string"Copy reference for support"
copiedLabel

Shown after copying the reference.

string"Copied"
copyText

Visible text of the copy button (`copyLabel` is its accessible name).

string"Copy"
fixLabel

Button text for a "fix" action without its own `actionLabel`.

string"Fix it"
detailsLabel

Text for the technical-details toggle.

string"Technical details"

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • The error is announced with role="alert"
  • Waiting and status changes are aria-live="polite"
  • Actions are buttons with visible text; icons are aria-hidden

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Make retried operations idempotent on the server and expose a status endpoint for ambiguous outcomes.

  • Explanations come from the catalog or explanation you pass
  • Whether a retry is safe depends on the `idempotent` flag you set

What it stops

Paid twice after a payment timeout error

Also known as

error message · error state · 500 · timeout · retry · support reference · payment failed · error boundary

Field type

Dependencies

Pairs well with