Error model
Two failure shapes, seven error classes, one decision flow. Understand the SDK's error contract once and stop second-guessing.
The SDK has two kinds of failure: thrown errors and error envelopes. They surface different things and want different handling. Once you know which is which, every error in the SDK follows the same pattern.
The two shapes
┌──────────────────────────────┐
│ bridge call (e.g. callApi) │
└──────────────┬───────────────┘
│
┌───────────────┴───────────────┐
│ │
┌─────────▼──────────┐ ┌──────────▼─────────┐
│ THROWN │ │ RETURNED IN ENVELOPE
│ • bridge crashed │ │ • backend said no │
│ • timeout │ │ • path not allowed │
│ • malformed reply │ │ • 4xx / 5xx │
│ • not in host │ │ • offline / timeout │
│ │ │ (host's, not SDK's)
│ try / catch this │ │ if (!r.success) ... │
└────────────────────┘ └─────────────────────┘Thrown — programmer or environment issues
The SDK throws when something is wrong with the runtime itself:
- The bridge isn't there (
NotInsideHostError). - The bridge call timed out (
BridgeTimeoutError). - The host returned a malformed payload (
InvalidResponseError). - The bridge transport itself crashed (
BridgeTransportError). - The car-status surface isn't supported on this host
(
CarStatusUnavailableError,CarStatusQuotaExceededError). - Privileged template not in your catalog (
UnknownTemplateError).
These are exceptional: you typically don't recover, you log and render a fallback.
Returned — operational failures
callApi() and admin invoke() return { success: false, error: ... }
when the operation failed but the bridge worked. This is
data:
const r = await client.callApi({ path: '/api/v1/x', method: 'GET' });
if (!r.success) {
// r.error.code: 'disallowed_path' | 'http_4xx' | 'http_5xx' | ...
}Don't try/catch around if (!r.success) — that conflates two
different failure modes and loses information.
The seven SDK error classes
| Class | When | What to do |
|---|---|---|
NotInsideHostError | No bridge on window. SSR, Storybook, Node. | Catch and render a fallback UI. Most common in test setups. |
BridgeTimeoutError | Host bridge didn't respond in time. Default 10 s. | Retry once with backoff, or surface "device unresponsive". error.operation says which call timed out. |
BridgeTransportError | Bridge layer crashed mid-call. Host bug. | Log, report. Usually transient — retry with backoff. Original cause is in error.cause. |
InvalidResponseError | Host returned a payload that doesn't match the SDK's schema. | Log + report. Means host/SDK version drift. Don't retry — same payload will fail again. |
CarStatusUnavailableError | Bridge isn't a CarStatusBridge (older host, test stub). | Catch on client.car.getStatus() etc.; render a fallback. |
CarStatusQuotaExceededError | More than 10 concurrent onStatusChange subs from this mini-app. | Find the leak. Almost always a missing useEffect cleanup. |
UnknownTemplateError (admin-sdk) | templateId not in your catalog snapshot. | Bug in your code or a stale catalog. Refresh the catalog or check the template name. |
All seven extend SDKError and carry a stable code field:
| Class | code |
|---|---|
NotInsideHostError | 'NOT_INSIDE_HOST' |
BridgeTimeoutError | 'BRIDGE_TIMEOUT' |
BridgeTransportError | 'BRIDGE_TRANSPORT' |
InvalidResponseError | 'INVALID_RESPONSE' |
CarStatusUnavailableError | 'CAR_STATUS_UNAVAILABLE' |
CarStatusQuotaExceededError | 'CAR_STATUS_QUOTA_EXCEEDED' |
UnknownTemplateError (admin-sdk) | 'UNKNOWN_TEMPLATE' |
The decision flow
┌──────────────────┐
│ caught an error? │
└────────┬─────────┘
│
┌──────────────┴───────────────┐
│ │
┌───▼────┐ ┌──────▼──────┐
│ thrown │ │ envelope │
│ (try/ │ │ (r.success │
│ catch) │ │ === false) │
└───┬────┘ └──────┬──────┘
│ │
switch (e.code) { switch (r.error.code) {
NOT_INSIDE_HOST: disallowed_path:
▶ render fallback ▶ log; config bug
BRIDGE_TIMEOUT: http_4xx:
▶ retry / banner ▶ surface message
CAR_STATUS_UNAVAILABLE: http_5xx:
▶ hide widget ▶ retry / banner
CAR_STATUS_QUOTA_EXCEEDED: network_error:
▶ check for leak ▶ "no signal"
INVALID_RESPONSE: timeout:
▶ log + report ▶ same as network
BRIDGE_TRANSPORT: <backend code>:
▶ log + retry ▶ render per-code
} }Switch on code, not instanceof
// Wrong — coupling to class names
if (e instanceof BridgeTimeoutError) { ... }
else if (e instanceof BridgeTransportError) { ... }
// Right — switch on the stable code
import { SDKError } from '@i99dash/sdk';
if (e instanceof SDKError) {
switch (e.code) {
case 'BRIDGE_TIMEOUT':
return retryWithBackoff();
case 'BRIDGE_TRANSPORT':
return reportTransient(e.cause);
case 'NOT_INSIDE_HOST':
return renderFallback();
// ...
}
}
throw e;code is part of the public API. New codes only get added in minor
releases; existing codes never change meaning. Class names work too,
but they're harder to switch on cleanly when you have many cases.
cause carries the underlying error
Every wrapped error sets cause per the ES2022 Error spec:
catch (e) {
if (e instanceof BridgeTransportError) {
Sentry.captureException(e, { extra: { cause: e.cause } });
}
}Sentry, modern browser DevTools, and most logging libraries walk the
.cause chain automatically. Don't redact it — the original cause is
the actionable part.
When to retry
| Class / code | Retry? | How |
|---|---|---|
BRIDGE_TIMEOUT | Maybe — once with backoff. Persistent timeout = host hung. | Single 500-1000 ms delay then retry. Two failures → surface. |
BRIDGE_TRANSPORT | Yes — usually transient. | Exponential backoff (200 → 400 → 800 ms). |
INVALID_RESPONSE | No. | The host will return the same broken payload. Log and stop. |
NOT_INSIDE_HOST | No. | Permanent on this runtime. |
CAR_STATUS_UNAVAILABLE | No. | Older host or test stub. Render fallback. |
CAR_STATUS_QUOTA_EXCEEDED | No. | Find the leak. |
envelope http_5xx | Yes. | Up to backend's idempotency contract. |
envelope network_error | Yes — typically with longer backoff. | Show progress. |
envelope disallowed_path | No. | Config bug; retry won't fix it. |
Related
- Best practices — Errors — code-style examples for each pattern.
- Errors reference — auto-generated from the
SDKErrorCodeunion. - Calling your backend — the
envelope contract for
callApi. - Subscriptions — error model for
client.car.*specifically.