v149 · Payments · demo
Error Recovery Lab
Request deliberate payment handler errors from a real same-origin payment app and observe how a well-written merchant page reacts — field highlighting, retry guidance, and graceful fallback for each error type enabled by Chrome 149.
Payment handler setup
Install the handler before pressing Pay. Each scenario calls PaymentRequest.show() from the Pay button.
Live checkout — Chrome 149 structured error reporting
scenario: insufficient_funds
Developer Conference Ticket£149.00
Booking fee£2.99
Total£151.99
Insufficient funds
The payment handler returned a structured error — your balance is below the charge amount.
Merchant action: show balance warning, offer split-payment or saved cards.
Card expired
The handler identified which field is invalid and returned it in the error object.
Merchant action: highlight
expiry field, prompt user to update card details.Network timeout — retry available
The handler set
retryable: true — the merchant can silently retry without user intervention.User cancelled
The browser surfaced
AbortError. This is user intent, not an internal payment app failure.Merchant action: do not retry automatically, keep the basket intact, and let the user continue shopping.
Plain message fallback
The handler did not emit JSON. Merchant code should surface the raw
err.message and use a generic fallback path.Merchant action: log the raw message, avoid assuming a payload schema, and offer another payment method.
handler fieldmerchant target
field: "expiry"#expiry.field-errortype: "insufficient_funds"#funds-error-objretry_after_ms#retry-status countdownRecovery Flow — how merchants should handle each error type
The JSON envelope shown here is a merchant/payment-handler convention, not a required wire format in the Payment Request spec. Chrome 149 preserves a richer
err.name and err.message; your app decides whether that message is JSON, a plain string, or another parseable format.
// Chrome 149 — payment handler reports structured error via DOMException
async function checkout(method) {
const req = new PaymentRequest([{ supportedMethods: method }], details);
try {
const response = await req.show();
await response.complete('success');
} catch (err) {
// err.name is 'AbortError' (user cancel) or 'OperationError' (handler error)
// err.message may carry JSON from your handler, but JSON is only a convention
if (err.name === 'AbortError') {
showCancelMessage('Your basket is saved.');
return;
}
let errorData = null;
try { errorData = JSON.parse(err.message); } catch (_) {
logRawPaymentHandlerMessage(err.message);
showFallbackPaymentMethod();
return;
}
switch (errorData.type) {
case 'insufficient_funds':
showMessage('Your balance is too low. Try a different card.');
break;
case 'card_expired':
highlightField(errorData.field); // e.g. "expiry"
showMessage('Card expired — please update your details.');
break;
case 'timeout':
if (errorData.retryable) {
await wait(errorData.retry_after_ms);
await checkout(method); // retry after visible back-off
} else {
showMessage('Payment service unavailable. Try again later.');
}
break;
}
}
}