Error handling

@hyperyai/sdk error handling — HyperyModals for automatic re-auth and funds modals, RestrictionModal, SpendingLimitAlert, InsufficientCreditsAlert, ErrorBoundary, useError, parseError and the is*Error helpers.

Hypery returns failures as { "error": { "code", "message", "type", ... } }. The SDK classifies them into a ParsedError and ships UI for the ones a user can fix: signing in again, adding funds, or adding a card.

There are two ways to wire it:

  1. Automatic — make requests with authenticatedFetch and mount <HyperyModals /> once. Nothing else to wire.
  2. Manual — catch errors yourself, keep them in useError, and render RestrictionModal or the inline alerts.

Error codes

CodeHTTPParsedError flagMeaning
INSUFFICIENT_CREDITS402isInsufficientCreditsCredit balance too low. Data: available, required.
PAYMENT_METHOD_REQUIRED402isPaymentMethodRequiredNo card on file.
PAYMENT_DECLINED402isPaymentDeclinedThe card was declined. Data: reason?.
SPENDING_LIMIT_EXCEEDED429isSpendingLimitA spending limit was hit. Data: limitType, limit, current, requested, resetsAt?.
RATE_LIMITED429isRateLimitRate limit. Also set for type: 'rate_limit_error'.
UNAUTHENTICATED401isAuthToken missing or revoked. Also set for type: 'authentication_error'.
PERMISSION_DENIED, INSUFFICIENT_SCOPE403isPermissionDeniedNot allowed — don't re-authenticate. Also set for type: 'permission_error' / 'authorization_error'.

When a body has no recognised code, parseError falls back to the HTTP status (if the object carries status, statusCode or response.status): 402 → INSUFFICIENT_CREDITS, 401 → UNAUTHENTICATED, 403 → PERMISSION_DENIED, 429 → RATE_LIMITED. Anything else becomes UNKNOWN_ERROR with the object's message.

See Spending limits and Credits for when the billing errors occur.

HyperyModals

Mount once anywhere inside HyperyProvider. It reads the provider state that authenticatedFetch sets:

Closing a modal clears the state.

import { HyperyProvider, HyperyModals, useAuth } from '@hyperyai/sdk';
 
<HyperyProvider config={config}>
  <App />
  <HyperyModals branding={{ appName: 'Acme' }} onRetry={() => window.dispatchEvent(new Event('retry'))} />
</HyperyProvider>;
 
function App() {
  const { authenticatedFetch, gatewayUrl } = useAuth();
  // A 402 here opens the funds modal automatically.
  const run = () => authenticatedFetch(`${gatewayUrl}/api/v1/chat/completions`, { method: 'POST', body: '…' });
  return <button onClick={run}>Run</button>;
}
PropTypeDescription
branding{ logo?, appName?, primaryColor? }Forwarded to AuthModal.
showSocialbooleanForwarded to AuthModal.
showEmailPasswordbooleanForwarded to AuthModal.
onRetry() => voidCalled from the funds modal's retry / Continue buttons — re-run the blocked request here.

All props are optional. If you pass onUnauthorized / onRestricted in the provider config the state is still set, so HyperyModals still opens.

RestrictionModal

The funds modal. When error is non-null it loads GET /api/wallet/state and shows the action that fits:

SituationAction shown
SPENDING_LIMIT_EXCEEDEDManage spending limits (opens the gateway billing page) + Try again
Otherwise, if the team is in metered or vag_passthrough mode, has no card, or the code is PAYMENT_METHOD_REQUIRED / PAYMENT_DECLINEDAdd a payment method (or Update payment method when a card exists) — Stripe-hosted Checkout in a popup
Otherwise (prepaid with a card, e.g. INSUFFICIENT_CREDITS)1-click $10 / $25 / $50 top-up + Other amount…

After funds or a card are added it shows Continue, which calls onRetry and onClose. Failures show the gateway's message (from { error: '…' }, { error: { message } } or { message } bodies). Renders nothing when error is null.

import { RestrictionModal, useAuth, useError } from '@hyperyai/sdk';
 
function Chat() {
  const { authenticatedFetch, getAccessToken, gatewayUrl, clientId } = useAuth();
  const { error, setError, clearError, isBillingRestriction } = useError();
 
  const send = async () => {
    const res = await authenticatedFetch(`${gatewayUrl}/api/v1/chat/completions`, { method: 'POST', body: '…' });
    if (!res.ok) setError({ ...(await res.json()), status: res.status });
  };
 
  return (
    <>
      <button onClick={send}>Send</button>
      <RestrictionModal
        error={isBillingRestriction && error ? { ...error.data, code: error.code, message: error.message } : null}
        clientId={clientId}
        gatewayUrl={gatewayUrl}
        getAccessToken={getAccessToken}
        onClose={clearError}
        onRetry={send}
      />
    </>
  );
}
PropTypeDescription
errorRestrictionError | nullRequired. The gateway's inner error object ({ code, message, type?, limitType?, limit?, current?, requested?, resetsAt?, available?, required?, ... }), or null to hide.
gatewayUrlstringRequired. Gateway base URL.
getAccessToken() => Promise<string | null>Required. From useAuth().
onClose() => voidRequired. Close / clear the error.
clientIdstringYour OAuth client id, sent as X-Hypery-Client-Id.
appIdstringDeprecated alias for clientId.
onRetry() => voidRetry the blocked request.
onFunded() => voidCalled after funds or a card are added.
classNamestringExtra classes on the dialog.
overlayClassNamestringExtra classes on the overlay.

RestrictionModal takes the raw error object, not a ParsedError. Pass the response body's error directly, or spread parsed.data as above.

Inline alerts

SpendingLimitAlert

An inline alert for SPENDING_LIMIT_EXCEEDED: the message, limitType usage (current / limit credits used) and the reset time. Renders nothing unless error.isSpendingLimit.

import { SpendingLimitAlert, parseError } from '@hyperyai/sdk';
 
<SpendingLimitAlert error={parseError(body)} onRetry={retry} onUpgradeLimits={() => router.push('/billing')} />
PropTypeDescription
errorParsedErrorRequired.
onRetry() => voidShows Try again.
onUpgradeLimits() => voidShows Increase limits →.
classNamestringExtra classes.

InsufficientCreditsAlert

An inline alert for INSUFFICIENT_CREDITS, showing available vs required. Renders nothing unless error.isInsufficientCredits.

import { InsufficientCreditsAlert } from '@hyperyai/sdk';
 
<InsufficientCreditsAlert error={parsed} onAddCredits={() => checkout({ kind: 'topup', usdAmount: 10 })} />
PropTypeDescription
errorParsedErrorRequired.
onAddCredits() => voidShows Add credits →.
classNamestringExtra classes.

ErrorBoundary

A React error boundary that also displays API errors:

  • Render errors — when a child throws during render it shows fallback, or the default alert with Try again (which resets the boundary, then calls onRetry). onError is called with the error and React's ErrorInfo.
  • error prop — when set, it parses error and renders SpendingLimitAlert, InsufficientCreditsAlert, or a generic alert with the message instead of children. Falsy renders children.
import { ErrorBoundary } from '@hyperyai/sdk';
 
// As a render-error boundary
<ErrorBoundary
  fallback={(error, reset) => <button onClick={reset}>Something broke — retry</button>}
  onError={(error, info) => Sentry.captureException(error)}
>
  <Result />
</ErrorBoundary>
 
// As an API-error display
<ErrorBoundary error={lastError} onRetry={retry} onAddCredits={topUp} onUpgradeLimits={openLimits}>
  <Result />
</ErrorBoundary>

In @hyperyai/sdk 1.1.5 ErrorBoundary was a plain component that required error and did not catch render errors. Existing error={…} usage still works.

PropTypeDescription
erroranyOptional. Anything parseError accepts; falsy renders children.
childrenReactNodeShown when there's no error.
fallbackReactNode | ((error: unknown, reset: () => void) => ReactNode)Rendered when a child throws. A function gets the thrown error and reset, which re-renders children. Default: the generic alert with Try again.
onError(error: unknown, info: React.ErrorInfo) => voidCalled when a child throws (e.g. report to Sentry).
onRetry() => voidPassed to the spending-limit and generic alerts.
onUpgradeLimits() => voidPassed to SpendingLimitAlert.
onAddCredits() => voidPassed to InsufficientCreditsAlert.
classNamestringExtra classes.

useError

Local error state with parsing built in.

import { useError } from '@hyperyai/sdk';
 
const { error, setError, clearError, isAuth, isBillingRestriction } = useError();
 
try {
  await doThing();
} catch (err) {
  setError(err); // parsed into a ParsedError; falsy clears it
}
ReturnsTypeDescription
errorParsedError | nullCurrent error.
setError(error: any) => voidParse and store; a falsy value clears.
clearError() => voidClear.
hasErrorbooleanerror !== null.
isSpendingLimit, isInsufficientCredits, isPaymentMethodRequired, isPaymentDeclined, isAuthbooleanFlags of the current error.
isBillingRestrictionbooleanAny of the four billing flags.

parseError

import { parseError } from '@hyperyai/sdk';
 
const res = await fetch(url, init);
if (!res.ok) {
  const parsed = parseError({ ...(await res.json()), status: res.status });
  if (parsed.isPaymentMethodRequired) openCardModal();
}

parseError(error: any): ParsedError accepts the response envelope ({ error: { code } }), an unwrapped error object ({ code }), or a thrown Error. Include status for the status fallback. It returns:

FieldType
code, messagestring
type, statusstring?, number?
isSpendingLimit, isInsufficientCredits, isPaymentMethodRequired, isPaymentDeclined, isAuth, isPermissionDenied, isRateLimitboolean
dataErrorData — the original error object (billing fields live here)

Predicates and formatting

Each predicate is (error: any) => boolean and runs parseError on its input.

FunctionTrue when
isSpendingLimitError(e)isSpendingLimit
isInsufficientCreditsError(e)isInsufficientCredits
isPaymentMethodRequiredError(e)isPaymentMethodRequired
isPaymentDeclinedError(e)isPaymentDeclined
isAuthError(e)isAuth
isPermissionDeniedError(e)isPermissionDenied
isRateLimitError(e)isRateLimit
isBillingRestriction(e)any of spending limit, insufficient credits, payment method required, payment declined

formatTimeUntilReset(resetsAt?: string): string formats an ISO time as in 3h 12m / in 45m; returns soon if it's in the past and '' if omitted.

import { formatTimeUntilReset } from '@hyperyai/sdk';
 
formatTimeUntilReset(parsed.data.resetsAt); // "in 2h 5m"

Mid-stream errors from streaming responses are covered in Streaming.

Next steps