Error Model
Every failure surfaces as a typed error. There are three the SDK defines, plus one native browser error, and they partition cleanly by layer: transport, contract, and cancellation.
HTTPError
Section titled “HTTPError”Thrown when the server returns a non-2xx response. Network failures (offline,
DNS, CORS) also surface as HTTPError, with status: 0. No response arrived,
so the thrown error’s message is kept in bodyText and carried into message
— for example HTTP 0 AbortError: This operation was aborted, or
HTTP 0 Error: The node refused to attest: HTTP 404 from a sealed fetch.
class HTTPError extends Error { name = 'HTTPError'; status: number; // HTTP status (0 for network errors) statusText: string; // status text (when status is 0: 'Network Error' or the thrown error's name) url: string; // the request URL that failed headers: Headers; // response headers bodyText?: string; // raw response body if available (capped ~64KB); the thrown message when status is 0 explanation?: string; // the node's own words — the body's `error`, or the thrown message when status is 0 toJSON(): { status; statusText; url; headers; bodyText? };}message is HTTP <status> <statusText>: <explanation>, for logs.
explanation is the same text without the status line, for a UI that shows
what the node said.
Since core 0.11.0-rc.56, an admin request the node refuses answers with the
status that names the refusal, where earlier releases answered almost every
one with 500:
| Status | Meaning |
|---|---|
400 |
The request is malformed. Sending it again cannot help. |
401 |
Not authenticated. The SDK retries once via refresh first. |
403 |
The node will not do this for this caller. |
404 |
What the request names does not exist on this node, or has not synced yet. |
409 |
The request contradicts current state: an expired or spent invitation, a removed member. |
413 |
The body is over the node’s limit. |
503 / 504 |
The node cannot serve this yet, for example with no member online to hand over a group key. |
Classifying a failure: classifyError
Section titled “Classifying a failure: classifyError”Most callers ask two things of a failure: what kind is it, and would the same
request work later? classifyError answers both for anything a catch hands
over: an HTTPError, an axios-style error, a browser TypeError from a
fetch that never connected, or a plain Error.
import { classifyError } from '@calimero-network/mero-js';
try { await sdk.admin.uploadBlob(file);} catch (err) { const { kind, retryable, message } = classifyError(err); if (kind === 'too-large') showError('That file is over the node\'s limit.'); else if (retryable) scheduleRetry(); else showError(message); // the node's own words}kind |
Status | retryable |
|---|---|---|
invalid |
400, other 4xx |
no |
unauthorized |
401 |
yes (after signing in) |
forbidden |
403 |
no |
not-found |
404 |
yes |
conflict |
409 |
no |
gone |
410 |
no |
too-large |
413 |
no |
rate-limited |
429 |
yes |
unavailable |
503, 504 |
yes |
server |
other 5xx |
yes |
unreachable |
0, or a TypeError |
yes |
unknown |
no status | yes |
retryable: false means the node chose the answer, so asking again gets the
same one. For following an invitation, use
admin.redeemInvitation,
which builds on this and also settles a join that timed out but landed.
AuthRevokedError
Section titled “AuthRevokedError”A subclass of HTTPError — terminal auth failure. Thrown when the node reports
that the refresh token was reused or revoked, meaning refresh can’t
recover. Because it extends HTTPError, instanceof HTTPError still matches;
check it first if you want to distinguish it.
class AuthRevokedError extends HTTPError { name = 'AuthRevokedError'; reason: string; // 'token_reuse' | 'token_revoked'}When this is thrown the SDK has already cleared its tokens (onAuthRevoked →
clearToken). Prompt the user to sign in again.
RpcError
Section titled “RpcError”Thrown by sdk.rpc.execute() when the WASM contract returns a JSON-RPC error.
class RpcError extends Error { name = 'RpcError'; code: number; // JSON-RPC error code type?: string; // optional error type from the contract data?: unknown; // optional structured error data}The inherited message holds the human-readable description from the contract.
Cancellation and timeouts
Section titled “Cancellation and timeouts”A request that exceeds timeoutMs, or one cancelled via an AbortSignal, does
not surface as a native AbortError. Every high-level client routes through
the HTTP client, which catches the underlying DOMException
(AbortError/TimeoutError) and re-throws it as an HTTPError with
status: 0 — the same shape as any other network failure. Match it as an
HTTPError whose status is 0:
try { await sdk.admin.getContexts(); // subject to the client's global timeoutMs} catch (err) { if (err instanceof HTTPError && err.status === 0) { // network failure, timeout, or cancellation }}status: 0 alone doesn’t distinguish a deliberate cancellation from a genuine
network failure — track the AbortController you called .abort() on if you
need to tell them apart. Per-call signal/timeoutMs overrides are a feature of
the low-level HttpClient;
the sdk.admin / sdk.auth / sdk.rpc facades use the single global
timeoutMs you pass to the constructor.
Full handling pattern
Section titled “Full handling pattern”import { HTTPError, AuthRevokedError, RpcError } from '@calimero-network/mero-js';
try { const result = await sdk.rpc.execute({ contextId: 'ctx-id', method: 'get_value', argsJson: { key: 'foo' }, }); return result;} catch (err) { if (err instanceof RpcError) { // Contract-level error — the WASM returned an error object console.error(`Contract error ${err.code}: ${err.message}`, err.data); } else if (err instanceof AuthRevokedError) { // Refresh token reused/revoked — session is over promptSignIn(); } else if (err instanceof HTTPError) { // Transport-level error — HTTP 4xx/5xx, or status 0 for a network // failure, timeout, or cancellation if (err.status === 0) console.warn('Network failure, timeout, or cancelled'); else if (err.status === 403) console.error('Permission denied'); else console.error(`HTTP ${err.status}: ${err.statusText}`); } else { throw err; // something unexpected }}Note on 401s
Section titled “Note on 401s”The SDK handles 401 Unauthorized internally: it triggers a token refresh
and transparently retries the original request (see
the token lifecycle).
Your code only sees a 401 HTTPError if the refresh itself fails (e.g. the
refresh token is also expired). In that case, call sdk.clearToken() and prompt
the user to re-authenticate. If the token was reused or revoked, you get an
AuthRevokedError instead.