Skip to content

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.

Thrown when the server returns a non-2xx response. Network failures (offline, DNS, CORS) also surface as HTTPError, with status: 0.

class HTTPError extends Error {
name = 'HTTPError';
status: number; // HTTP status (0 for network errors)
statusText: string; // status text ('Network Error' when status is 0)
url: string; // the request URL that failed
headers: Headers; // response headers
bodyText?: string; // raw response body if available (capped ~64KB)
toJSON(): { status; statusText; url; headers; bodyText? };
}

Common statuses: 401 (not authenticated — the SDK retries via refresh first), 403 (insufficient permissions), 404 (not found), 422 (validation).

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 (onAuthRevokedclearToken). Prompt the user to sign in again.

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.

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.

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
}
}

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.