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-error
type: "insufficient_funds"#funds-error-obj
retry_after_ms#retry-status countdown
Recovery 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;
    }
  }
}

see also