Pho Design System

Error

One way to show a server error (4xx / 5xx). Photon services answer failures with an RFC 9457 problem bodycode, title, detail, remediation, requestId — and these components render that contract directly: hand them the caught value and the reviewed server copy appears; anything else falls back to a generic line instead of leaking a raw message.

Two presentations, one per altitude. Error is the inline line for forms, cards, and action rows; ErrorView is the failed state of a region or page, built on Status View. Both render nothing while the error is nullish, so they can sit unconditionally next to a query or mutation.

Client-side validation is not this component’s job — Field.Error handles messages wired to control validity.

Import

import { Error, ErrorView } from "@photon-ai/pho-ui/components/error";

Inline

Place Error where the failure happened. It announces via role="alert", shows the problem’s detail and remediation sentence, and links the problem’s documentation when it carries one.

Request rate limit exceeded. Wait a moment before trying again. Learn more
const { mutate, error } = useMutation(…);

<Error error={error} />

Details

When the caught value carried a problem (or at least a request id), the line ends with an info glyph. Hovering or focusing it opens the request’s fuller story: the problem title, the untruncated description, then status / code and the copyable request id grouped below for a support handoff. A generic unknown has nothing more to disclose, so the glyph doesn’t render. The panel is a Popover using the default Title / Description styles (its copy button is interactive), opened on hover so the gesture stays tooltip-light.

Photon is temporarily unreachable.
// Nothing to wire — the disclosure derives from the same `error` value.
<Error error={error} />

Fallback

When the response was not ok but the body wasn’t a contract problem, the client still knows the HTTP status — title becomes the reason phrase (e.g. “Bad Gateway”), the description explains it in a sentence, and the details panel also shows the numeric status. A true unknown (network failure, unexpected exception) stays the generic line with nothing more to disclose. Raw Error messages are never shown; pass children to override the copy.

The gateway got an invalid response from an upstream server.
Something went wrong.
// HTTP status known, no problem body → reason phrase + status in details
<Error error={apiError} />
// No status at all → generic line only
<Error error={new TypeError("ECONNREFUSED 10.0.0.1:443")} />
<Error error={error}>Couldn't save your changes.</Error>

Error view

For a region or page that failed to load — Status View with the error wiring done: warning icon, role="alert", title and description from the problem. onRetry renders a “Try again” button, the problem’s documentation renders a “Learn more” link, and the request id sits below as a footnote for support.

Too Many Requests
Request rate limit exceeded. Wait a moment before trying again.Request ID:
Learn more
const { data, error, refetch } = useQuery(…);

if (error) {
  return <ErrorView error={error} onRetry={() => refetch()} />;
}

Sizes

Status View’s axis, passed through — sm for cards and panels, the default base for whole pages.

Service Unavailable
Photon is temporarily unreachable.Request ID:
sm
Service Unavailable
Photon is temporarily unreachable.Request ID:
base
<ErrorView size="sm" error={error} onRetry={() => refetch()} />
<ErrorView size="base" error={error} onRetry={() => refetch()} />

Actions

actions replaces the built-in row when the way out isn’t a retry — a route back, a reload, an upgrade.

Service Unavailable
Photon is temporarily unreachable.Request ID:
<ErrorView
  error={error}
  actions={
    <>
      <Button size="sm" onClick={() => window.location.reload()}>
        Reload
      </Button>
      <Button
        size="sm"
        variant="outlined"
        onClick={() => window.history.back()}
      >
        Back
      </Button>
    </>
  }
/>

Parsing

The contract helpers are exported for surfaces beyond these two — branching on code, or feeding a toast. parseProblem finds a problem carried by the value itself, typed onto an error as a problem field (an ApiError shape), or along an Error cause chain; describeError turns any caught value into displayable copy.

import {
  describeError,
  parseProblem,
  type Problem,
} from "@photon-ai/pho-ui/components/error";

const problem = parseProblem(error);
if (problem?.code === "NOT_AUTHENTICATED") {
  redirectToSignIn();
}

const { title, description } = describeError(error);
toast.error(title, { description });

Guidelines