Error Handling
App Bridge errors fall into three categories:
- Transport errors — the host never responds. The SDK rejects with a
timeout after 10 seconds.
- Host-reported errors — the host runs your action but the underlying
operation fails. The SDK rejects with an
Error carrying the host’s
reason string.
- User-cancellation — the host returns a successful response whose
payload signals the user dismissed the prompt. These are not thrown;
they resolve normally with a cancel signal (an empty
selection, a
close/cancel event on the action class, etc. — Scanner is the
exception: its capture() rejects on cancel). Treat them as a normal
branch.
Knowing which category you’re in determines whether to retry, surface a
toast, or render an empty state.
How errors flow through dispatchAndWait
The wire format for an error response is:
When error is present on the response, the SDK rejects the promise with
new Error(response.error) so callers can use a standard try / catch:
On error, the host’s error field is forwarded to the callback,
and dispatchAndWait translates that into a rejection. Subscriptions
created via subscribe() also receive the error as the second argument
of the callback — handle it explicitly when subscribing.
Timeouts
dispatchAndWait rejects with Error('App Bridge: timeout waiting for <ACTION> response') after 10 000 ms if the host never responds.
The timeout is hard-coded; you cannot tune it per call. Three things
commonly cause a timeout:
- Action is unsupported on the current host. The admin host doesn’t
wire
CART_LINES_CHANGE; the checkout host doesn’t wire
RESOURCE_PICKER_OPEN; the post-purchase host doesn’t wire
MODAL_OPEN. Use the host capability table in each host’s reference
page to feature-gate calls.
- The host hasn’t mounted yet. Checkout extensions sometimes
dispatch before the checkout extension slot finishes mounting. Wait
for
BRIDGE_PING once before issuing real calls.
- A typo in the action name. Action names are case-sensitive and
the host silently drops unknown names.
'TOAST_SHO' will time out
ten seconds later — there is no synchronous “unknown action” error.
Recovery pattern:
Do not retry user-driven actions (modals, pickers, prints) — the
user has already moved on. Only retry data-fetch RPCs (USER_FETCH,
CONFIG_FETCH, ENVIRONMENT_FETCH, CART_GET, etc.) where a missed
response means stale state.
Subscription errors
app.subscribe(action, callback) forwards the host’s error string as
the second argument:
Forgetting the second parameter means error responses look like normal
data — your component will then read the empty payload ({}) and
silently render an empty cart. Always destructure (payload, error).
Per-action failure modes
Toast
Toast.create(app, { … }).dispatch() is fire-and-forget — it doesn’t
return a promise, so transport failures are invisible. If the toast
must succeed (regulatory notice, payment receipt), use
dispatchAndWait('TOAST_SHOW', …) and surface a fallback in catch.
The admin host reads payload.type; the checkout host reads
payload.variant. The Toast helper sends type; in checkout iframes
build the payload manually.
Modal / ConfirmationModal
A dismissal (backdrop click / Escape) fires the close event — treat it
as a normal cancel, not an error. If no event ever fires, the host
never opened the modal at all — usually because the iframe is in a
context that doesn’t support modals (checkout, post-purchase).
Resource Picker
An empty selection array is the user-cancel signal — not an error
(there is no cancelled field on the payload; class consumers get the
dedicated cancel event instead).
A rejected promise from the picker usually means the host doesn’t
support the requested resourceType for that surface — admin-only
types (order, customer, etc.) on a non-admin host will time out.
Clipboard
Clipboard.write() runs iframe-side and silently falls back to the
legacy document.execCommand('copy') path when the modern API throws
or is unavailable. The promise still resolves either way — there is no
indication to the caller that the legacy path ran.
Clipboard.read() (and the paste() alias) does reject when the
host denies the request — Chrome blocks clipboard-read for cross-
origin iframes, so the SDK delegates to the host, and the host can
reject if the user denies the permission prompt or the focused window
isn’t authorised. Errors arrive as
Error('clipboard read denied') or similar.
Cart (checkout / post-purchase)
Cart.applyCartLinesChange, applyDiscountCodeChange,
applyNoteChange, applyAttributeChange, applyMetafieldChange all
throw when the host’s verify-cart pipeline rejects the mutation. Common
reasons:
- A
cart_transform or order_validation function blocks the change.
- The merchant’s discount rules reject the code (expired, min-cart not
met, mutually exclusive with another active discount).
- The line variant is sold out or no longer purchasable.
The host returns:
…which the SDK forwards as new Error('verify-cart blocked: out of stock').
Always wrap cart mutations in try / catch and re-fetch the cart
(CART_GET) on failure so your UI mirrors host state.
Gift cards are live on the checkout host: addGiftCard and
removeGiftCard (via GIFT_CARD_CHANGE) resolve with { ok: true, code }
on success and reject with the host’s error message on failure.
removeDiscountCode is not wired — it does not resolve { ok: false },
it rejects with Error('not supported yet'). Wrap it in try / catch
if you attempt it. (addDiscountCode is live.)
METAFIELD_CHANGE is fully wired on the checkout host: it accepts
{ type: 'updateMetafield' | 'removeMetafield', namespace, key, ... }
(plus set/remove aliases) and rejects with
'namespace and key required' or
'unsupported METAFIELD_CHANGE type "<type>"' on bad payloads.
BuyerJourney
The intercept callback can be sync or async. If it throws, the SDK
defaults to behavior: 'allow' so the buyer is never trapped by a
broken extension. Watch your console — silent intercept failures cause
your business rule to be bypassed.
RestApi
RestApi.fetch() reuses the standard Fetch API — it does not throw
on non-2xx. Inspect response.ok yourself, or use fetchJson() which
rejects on non-2xx with a body-aware message:
Two distinctive failures:
'RestApi: host CONFIG_GET did not return apiBase' — the host
responded but the payload lacks apiBase. The SDK clears its config
cache so the next call retries. Verify the host implements
CONFIG_GET.
'App Bridge: timeout waiting for SESSION_TOKEN_REQUEST response'
— the host never returned a JWT. Most often happens when the iframe
is in a host that doesn’t authenticate apps (checkout, post-purchase).
Intents
Intents.launch() is fire-and-forget — it returns the postMessage id
synchronously and never throws (beyond the empty-target guard).
Intents.launchAndWait() rejects on timeout or when the target
doesn’t resolve — a missing target never resolves with an { error }
payload, and there is no { cancelled: true } response:
The target is an admin-extension target string (e.g.
admin.product-details.action.my-action). Handle every failure in the
catch — do not test the resolved value for error or cancelled
fields; they don’t exist.
Session Token
app.getSessionToken() and useSessionToken().getToken() both throw
when the host rejects the JWT request — typically because the API
key / shop combination is invalid. Catch once at the top of your data
layer and surface a “reconnect” CTA:
The wrapper caches tokens until 30s before their JWT exp claim, so a
single failure doesn’t trigger a retry storm.
Error reference
Patterns
Show toast on transient failure, propagate on persistent
Gracefully degrade when an action isn’t wired
Surface BuyerJourney intercept failures without trapping the buyer
See Also