Skip to main content

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.

FieldTypeDescription
codeRiftApiErrorCodeStable string discriminator. The primary identifier for application logic.
statusnumberHTTP status. For infrastructure / fetch retry logic; not for app branching.
detailstringHuman-readable per-occurrence explanation. English-only; not for end-user copy.
typestringRFC 9457 §3.1.1 — problem-type URI. Defaults to "about:blank".
titlestringRFC 9457 §3.1.2 — short stable summary per type. Suitable for localization tables keyed by code.
instancestring | undefinedRFC 9457 §3.1.5 — URI identifying the specific occurrence. Useful for support correlation.
extensionsRecord<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.
messagestringBuilt 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

CodeStatusExtensionsWhenRecovery
validation_error400{ issues: readonly RiftValidationIssue[] }Request body / params failed server-side validation.Surface field errors from extensions.issues keyed by path.
altcha_payload_missing403Record<string, never>Captcha-gated endpoint hit without an altcha-payload header.Mount <CaptchaWidget>; the SDK widget auto-handles this.
altcha_payload_invalid403Record<string, never>Captcha solution failed signature verification.Widget auto-refreshes; retry.

Server-thrown — resource not found

CodeStatusExtensionsWhenRecovery
not_found404Record<string, never>Generic catch-all for unknown resource path.Verify the URL and resource ID.
event_not_found404Record<string, never>eventId doesn't resolve or has no waves configured.Confirm the ID and that the event has live waves.
order_not_found404Record<string, never>Referenced order doesn't exist.Restart the flow; the URL is probably stale.

Server-thrown — reservation flow

CodeStatusExtensionsWhenRecovery
insufficient_inventory409{ 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_reservation409Record<string, never>The reservation was refused (sold out, race, etc.).Refetch availability; restart selection.
reservation_no_longer_valid410Record<string, never>Order already terminal — defense against enumeration.Route the user forward.
reservation_token_expired410Record<string, never>Reservation token's TTL passed.Restart the reservation flow.
reservation_token_tampered401Record<string, never>Token signature failed verification.Restart the reservation flow.
max_per_order_exceeded422{ 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

CodeStatusExtensionsWhenRecovery
order_in_terminal_state409{ orderStatus: "complete" | "failed" | "refunded" }Previous attempt already completed.Route the user forward based on orderStatus.
checkout_window_closed412Record<string, never>Past checkoutDeadline; reservation may still be alive but checkout cannot start.Fire on.checkoutWindowClosed; restart reservation.

Server-thrown — post-purchase polling

CodeStatusExtensionsWhenRecovery
order_status_token_expired410Record<string, never>Order-status JWT's TTL passed (~10 min).Stop polling; route to email-link recovery.
order_status_token_tampered401Record<string, never>Token signature failed, or sub mismatch with the path orderId.Stop polling; route to email-link recovery.

Server-thrown — attendee identity

CodeStatusExtensionsWhenRecovery
attendee_unclaimable409{ 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

CodeStatusExtensionsWhenRecovery
rate_limited429{ retryAfterSeconds: number }Per-identity rate limit exceeded.Wait retryAfterSeconds (also in Retry-After header); retry.
service_unavailable503Record<string, never>Reservations are temporarily unavailable.Retry with backoff.

Authentication

CodeStatusExtensionsWhenRecovery
unauthenticated401Record<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)

CodeStatusExtensionsWhenRecovery
captcha_unavailable0Record<string, never>Captcha-gated request fired with no solution available.Mount <CaptchaWidget> and wait for useCaptcha().isReady. Set requireCaptcha on <ReserveButton> to enforce.
unknown0 or non-2xxRecord<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 code field matches an SDK-side-only code. A hostile or misconfigured server cannot impersonate captcha_unavailable or unknown to bypass the SDK's own narrowing — such payloads fall through to unknown with 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,
});
}