Errors
RiftApiError is the single typed error class every server failure
becomes. This page lists every registered code, the HTTP status
it surfaces with, the typed extensions shape it carries, what
triggers it, and how to recover. Pair with the
error-handling guide for the
narrowing pattern.
Import
import {
isRiftApiError,
RiftApiError,
type RiftApiErrorCode,
type RiftApiErrorExtensions,
} from "@feelrift/react";
RiftApiError shape
The class mirrors the server's RFC 9457 Problem Details
wire shape, plus Rift's code discriminator and a typed extensions
bag.
| Field | Type | Description |
|---|---|---|
code | RiftApiErrorCode | Stable string discriminator. The primary identifier for application logic. |
status | number | HTTP status. For infrastructure / fetch retry logic; not for app branching. |
detail | string | Human-readable per-occurrence explanation. English-only; not for end-user copy. |
type | string | RFC 9457 §3.1.1 — problem-type URI. Defaults to "about:blank". |
title | string | RFC 9457 §3.1.2 — short stable summary per type. Suitable for localization tables keyed by code. |
instance | string | undefined | RFC 9457 §3.1.5 — URI identifying the specific occurrence. Useful for support correlation. |
extensions | Record<string, unknown> | RFC 9457 §3.2 extension members. Class-level type is broad; narrow via isRiftApiError(err, "code") for typed access. |
name | "RiftApiError" | Inherited from Error. Useful for serialization. |
message | string | Built as ${code}: ${detail} for debugging. Not for end-user copy. |
Type guards
isRiftApiError(err)
Narrows err to RiftApiError. Use in the outer catch clause
before any code-specific handling.
try {
await confirm({ email });
} catch (err) {
if (!isRiftApiError(err)) throw err;
// err is RiftApiError
}
isRiftApiError(err, code)
Narrows err AND err.extensions to the shape registered for
code in RiftApiErrorExtensions. The recommended form for every
per-code branch.
if (isRiftApiError(err, "rate_limited")) {
retryAfter(err.extensions.retryAfterSeconds); // typed: number
}
isKnownRiftApiErrorCode(code)
Runtime predicate that returns true when code is a string
literal the SDK recognizes. Useful for narrowing arbitrary error
payloads parsed by other tooling:
import { isKnownRiftApiErrorCode } from "@feelrift/react";
const code = response.body.code;
if (isKnownRiftApiErrorCode(code)) {
// code is RiftApiErrorCode
}
Error catalog
Every registered code, organized by source. The "Extensions" column
shows the registered shape via RiftApiErrorExtensions["<code>"];
codes with no typed extensions resolve to Record<string, never>.
Server-thrown — input / payload
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
validation_error | 400 | { issues: readonly RiftValidationIssue[] } | Request body / params failed server-side validation. | Surface field errors from extensions.issues keyed by path. |
altcha_payload_missing | 403 | Record<string, never> | Captcha-gated endpoint hit without an altcha-payload header. | Mount <CaptchaWidget>; the SDK widget auto-handles this. |
altcha_payload_invalid | 403 | Record<string, never> | Captcha solution failed signature verification. | Widget auto-refreshes; retry. |
Server-thrown — resource not found
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
not_found | 404 | Record<string, never> | Generic catch-all for unknown resource path. | Verify the URL and resource ID. |
event_not_found | 404 | Record<string, never> | eventId doesn't resolve or has no waves configured. | Confirm the ID and that the event has live waves. |
order_not_found | 404 | Record<string, never> | Referenced order doesn't exist. | Restart the flow; the URL is probably stale. |
Server-thrown — reservation flow
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
insufficient_inventory | 409 | { unavailableTicketTypeIds: readonly string[] } | Requested tickets aren't available — sold out, or no wave is currently selling them. | Refetch availability; show "sold out" / "not on sale yet" for the listed ticket types. |
invalid_reservation | 409 | Record<string, never> | The reservation was refused (sold out, race, etc.). | Refetch availability; restart selection. |
reservation_no_longer_valid | 410 | Record<string, never> | Order already terminal — defense against enumeration. | Route the user forward. |
reservation_token_expired | 410 | Record<string, never> | Reservation token's TTL passed. | Restart the reservation flow. |
reservation_token_tampered | 401 | Record<string, never> | Token signature failed verification. | Restart the reservation flow. |
max_per_order_exceeded | 422 | { violations: readonly RiftPerOrderViolation[] } | One or more ticket types exceed their per-order cap. | Show each violations[].maxPerOrder; reduce the offending rows. |
Server-thrown — checkout flow
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
order_in_terminal_state | 409 | { orderStatus: "complete" | "failed" | "refunded" } | Previous attempt already completed. | Route the user forward based on orderStatus. |
checkout_window_closed | 412 | Record<string, never> | Past checkoutDeadline; reservation may still be alive but checkout cannot start. | Fire on.checkoutWindowClosed; restart reservation. |
Server-thrown — post-purchase polling
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
order_status_token_expired | 410 | Record<string, never> | Order-status JWT's TTL passed (~10 min). | Stop polling; route to email-link recovery. |
order_status_token_tampered | 401 | Record<string, never> | Token signature failed, or sub mismatch with the path orderId. | Stop polling; route to email-link recovery. |
Server-thrown — attendee identity
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
attendee_unclaimable | 409 | { attendeeId: string } | Attendee row matching the user's verified email is already linked to another account. | Surface "wrong account" message; user must sign in with the account that owns the email. |
Server-thrown — rate / availability
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
rate_limited | 429 | { retryAfterSeconds: number } | Per-identity rate limit exceeded. | Wait retryAfterSeconds (also in Retry-After header); retry. |
service_unavailable | 503 | Record<string, never> | Reservations are temporarily unavailable. | Retry with backoff. |
Authentication
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
unauthenticated | 401 | Record<string, never> | SDK-side: endpoint requires auth: "required" but no session is active. Server-side: attendee bearer token was rejected by the API (revoked, expired, tampered). | Trigger sign-in via useSignIn() or <SignInButton>; configure auth on <RiftProvider> if you haven't. |
SDK-side only (thrown before the request leaves)
| Code | Status | Extensions | When | Recovery |
|---|---|---|---|---|
captcha_unavailable | 0 | Record<string, never> | Captcha-gated request fired with no solution available. | Mount <CaptchaWidget> and wait for useCaptcha().isReady. Set requireCaptcha on <ReserveButton> to enforce. |
unknown | 0 or non-2xx | Record<string, never> | Transport error (0), non-JSON body, or unrecognized server code. The fallback for "something went wrong, we don't know what". | Treat as generic; if persistent and the SDK is up-to-date, file an issue with the network response. |
Note The SDK rejects server payloads whose
codefield matches an SDK-side-only code. A hostile or misconfigured server cannot impersonatecaptcha_unavailableorunknownto bypass the SDK's own narrowing — such payloads fall through tounknownwith the original HTTP status preserved.
Validation issue shape
extensions.issues on validation_error carries one row per
rejected field:
type RiftValidationIssue = {
path: readonly (string | number)[]; // e.g. ["items", 0, "quantity"]
message: string; // e.g. "Required"
code?: string; // optional discriminator the server may emit
};
To group by field for inline errors:
const errorByPath = new Map<string, string>();
for (const issue of err.extensions.issues) {
errorByPath.set(issue.path.join("."), issue.message);
}
Per-order violation shape
extensions.violations on max_per_order_exceeded carries one row per
ticket type that breached its per-order cap:
type RiftPerOrderViolation = {
ticketTypeId: string; // which ticket type exceeded its cap
requested: number; // how many were requested
maxPerOrder: number; // the configured cap for that ticket type
};
A single reserve can breach several ticket types at once, so handle the list — flag every offending row rather than only the first.
Common patterns
Switch on code with full type narrowing
import { isRiftApiError } from "@feelrift/react";
function handle(err: unknown) {
if (!isRiftApiError(err)) throw err;
if (isRiftApiError(err, "rate_limited")) {
return { kind: "retry", afterSeconds: err.extensions.retryAfterSeconds };
}
if (isRiftApiError(err, "insufficient_inventory")) {
return {
kind: "sold_out",
ticketTypeIds: err.extensions.unavailableTicketTypeIds,
};
}
if (isRiftApiError(err, "max_per_order_exceeded")) {
return { kind: "cap_exceeded", violations: err.extensions.violations };
}
if (isRiftApiError(err, "validation_error")) {
return { kind: "validation", issues: err.extensions.issues };
}
return { kind: "generic" };
}
Localized message keyed by code
import type { RiftApiError } from "@feelrift/react";
function localize(t: TFunction, err: RiftApiError): string {
return t(`error.${err.code}`, {
fallback: t("error.generic"),
// RFC 9457 extensions flatten to top level — pass the whole bag
// so message templates can interpolate.
...err.extensions,
});
}
Related
- Error handling guide — the contract for consumer handlers.
<RiftProvider>—on.error— global catch-all for observability.- RFC 9457 Problem Details for HTTP APIs.