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):
- Already a
SymmioRequestError— passthrough. SymmApiError(HTTP failure) →kind: "api"withstatus,code,responseData,retryAfterMs.- Any other
SymmError(config,validation, or a non-HTTPapifailure) →kind: "sdk"with the SDKcode, even when itscauseis a viem error. - A viem
BaseError, thrown directly or found in thecausechain, inspected withBaseError.walk:UserRejectedRequestError→kind: "user-rejected".ContractFunctionRevertedError→kind: "contract-revert"withreason(the revert reason, or the custom error name).InsufficientFundsError→kind: "insufficient-funds".- Anything else (HTTP transport, timeout, gas estimation, execution failure) →
kind: "rpc"withshortMessage.
- Any other
Error→kind: "unknown"with itsmessage. - A non-
Errorvalue →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.
Related
- Core errors — full
SymmError/SymmApiErrorcatalogue. - Hook pattern — where the normalization runs.