Error Handling
When the host rejects, how errors are encoded on the wire, and how the web maps them.
On the wire
The two transports encode errors out-of-band rather than as a thrown JS value, so a rejection is a string, not a structured object.
- iOS —
WKScriptMessageHandlerWithReplycallsreplyHandler(nil, errorMessage). The non-nilerrorMessagerejects the web-side Promise; its string becomes the rejection reason. - Android — the injected shim rejects its returned Promise with an error string supplied by the host.
A host MUST provide a human-meaningful, non-empty error string when it rejects. It SHOULD NOT leak secrets or raw stack traces into that string.
Which capabilities reject
| Capability | Rejects? | When |
|---|---|---|
track | No | Best-effort; resolves undefined regardless of delivery |
headers | No | "Nothing to inject" is {}, not an error |
refreshToken | Yes | Terminal auth failure (expired/revoked refresh token, signed out) |
unknown type | No | Resolves undefined |
In practice refreshToken is the only v1 capability that rejects under normal
operation. The host-unavailable case (no channel on window) is surfaced by the
SDK, not the wire.
How the SDK maps errors
The web client normalises every failure into a BridgeError or one of its
subclasses:
export class BridgeError extends Error {}
// No native channel on window (desktop browser, non-app WebView):
export class BridgeUnavailableError extends BridgeError {}
// Host replied, but the payload did not match the capability's result shape:
export class BridgeProtocolError extends BridgeError {}| Error class | Cause | Force re-auth? |
|---|---|---|
BridgeUnavailableError | No native channel on window | No — no host |
BridgeError | Host rejected (terminal auth failure) | Yes |
BridgeProtocolError | Host replied with a malformed / empty payload | No — host bug |
- A wire rejection (the host rejected) becomes a
BridgeErrorwhosemessageis the host-supplied string. ForrefreshTokenthis is a terminal auth failure — re-authenticate the user. - A missing host becomes a
BridgeUnavailableError.getHeadersdoes not throw it (it resolves{});refreshTokenand a forcedfetchretry surface it. - A malformed reply — the host resolved, but the value did not match the
expected result shape (e.g.
refreshTokenreturning a value without a stringauthorization, or theundefinedan untrusted-surface or sub-frame caller receives) — becomes aBridgeProtocolError. This is a protocol/serialization bug on the native side or a denied trust gate, not a signed-out user. Callers MUST NOT force re-authentication on it.
Patterns
Refresh + retry once. A second 401 is terminal — surface it, do not loop.
try {
const { authorization } = await bridge.refreshToken()
return await retryWith(authorization)
} catch (e) {
if (e instanceof BridgeUnavailableError) {
// no host — fall back to the web sign-in flow
} else if (e instanceof BridgeProtocolError) {
// malformed reply — a host bug, not a signed-out user. Do NOT re-auth.
} else {
// terminal auth failure — send the user to re-authenticate
}
throw e
}Never let track reject. Fire-and-forget means the web swallows any
rejection so it cannot become an unhandledrejection:
bridge.track('event_name', params) // returns void; the SDK catches internallyDistinguish "no host" from "real failure". Branch on
BridgeUnavailableError when the two cases need different UX — local development
(no host) versus a genuine auth failure in the app.