Skip to Content
Symmio Trading-SDK — the SDK surface for builders on Arbitrum
ReactReact ConceptsError normalization

Error normalization

Every hook wraps its underlying core action and pipes failures through normalizeSymmError — a classifier that turns any thrown exception (viem, axios, SDK, user rejection) into a single error class: SymmioRequestError. Consumers handle errors by switching on .kind instead of learning multiple error hierarchies.

SymmioRequestError

export type SymmioNormalizedErrorKind = | "user-rejected" | "contract-revert" | "insufficient-funds" | "rpc" | "api" | "sdk" | "unknown"; export class SymmioRequestError extends Error { readonly name = "SymmioRequestError"; readonly kind: SymmioNormalizedErrorKind; readonly code?: string; // SDK error code, for kind "api" or "sdk" readonly status?: number; // HTTP status, for kind "api" readonly responseData?: unknown; // raw response body, for kind "api" readonly retryAfterMs: number | null; // Retry-After delay in ms, for kind "api"; null otherwise readonly reason?: string; // revert reason, for kind "contract-revert" readonly shortMessage?: string; // viem short message, for kind "rpc" }

It is one class, not a union of shapes: kind tells which of the optional fields are set.

Every hook’s error typed as SymmioRequestError | null. Discriminate on .kind:

const mutation = useAllocate(); if (mutation.error) { switch (mutation.error.kind) { case "user-rejected": return null; // silent — user dismissed case "contract-revert": return <Toast type="error">Revert: {mutation.error.reason ?? mutation.error.message}</Toast>; case "api": return ( <Toast type="error"> API {mutation.error.status}: {mutation.error.message} </Toast> ); default: return <Toast type="error">{mutation.error.message}</Toast>; } }

normalizeSymmError

import { normalizeSymmError } from "@symmio/trading-react"; const normalized = normalizeSymmError(someUnknownError);

Classification order (first match wins):

  1. Already a SymmioRequestError — passthrough.
  2. SymmApiError (HTTP failure) → kind: "api" with status, code, responseData, retryAfterMs.
  3. Any other SymmError (config, validation, or a non-HTTP api failure) → kind: "sdk" with the SDK code, even when its cause is a viem error.
  4. A viem BaseError, thrown directly or found in the cause chain, inspected with BaseError.walk:
    • UserRejectedRequestError → kind: "user-rejected".
    • ContractFunctionRevertedError → kind: "contract-revert" with reason (the revert reason, or the custom error name).
    • InsufficientFundsError → kind: "insufficient-funds".
    • Anything else (HTTP transport, timeout, gas estimation, execution failure) → kind: "rpc" with shortMessage.
  5. Any other Error → kind: "unknown" with its message.
  6. A non-Error value → kind: "unknown", with the value itself as the message when it is a string.

The original error is preserved on cause — logging or DevTools panels still get the full viem / axios / whatever stack. For an HTTP failure that is the SymmApiError, whose own cause is the axios error with its request config, headers and body included. Gasless requests are the exception: their cause is a sanitized snapshot without them. responseData is the raw response body either way, and can echo request fields, so redact errors before forwarding them to third-party logging.

Rate limits and retryAfterMs

retryAfterMs carries the server’s Retry-After delay in milliseconds for an "api" error, and is null for every other kind. It is also null when the header is absent or unreadable: a cross-origin browser only sees it when the server exposes it. Treat null as unknown, not as “retry now”:

if (error?.kind === "api" && error.status === 429) { const delayMs = error.retryAfterMs ?? 1_000 + Math.random() * 500; setTimeout(() => refetch(), delayMs); }

Why not instanceof?

  • Works across realms (SSR, iframes, package duplication).
  • Works with any user-space custom error (e.g. wallet connectors that throw their own types).
  • Compact — one type, one switch.

instanceof SymmError still works if you prefer it inside a manual try/catch around a core action; the React layer just makes the discriminated form the default so switch on .kind is always safe.

Last updated on