← Chrome 149 reference

v149 · web api · shipped

Payment Handler Internal Error Reporting

Payment handlers can now distinguish between "user cancelled" and "internal payment app error" when rejecting PaymentRequestEvent.respondWith(). Merchants receive an OperationError for app failures rather than the generic AbortError used for user cancellation — enabling more accurate error recovery flows.

at a glance

Shipped inChrome 149 / Edge 149
StatusEnabled by default
FlagNone
SpecW3C Payment Request — §"Payment handler indicates an internal error"
ChromeStatus5942637229113344 — Allow payment handlers to report back internal errors

why it exists

The Payment Handler API lets a web-based payment app respond to PaymentRequest.show() by handling the paymentrequest event in a service worker and calling PaymentRequestEvent.respondWith(promise). Before Chrome 149, any rejection of that promise — whether caused by the user pressing "cancel" or by a crash inside the payment app — reached the merchant as an AbortError. There was no way to tell the two apart.

This matters because the correct merchant response differs: a user cancellation should keep the checkout flow open so the user can choose a different payment method, while an internal payment app error might warrant an error message, a fallback, or a retry. The new behaviour gives payment handlers a dedicated signal for internal failures.

Source: chromestatus and blink-dev Intent to Ship thread.

shape of the change

Error type contract

Rejection with AbortError (or any non-OperationError) Treated as user cancellation. PaymentRequest.show() rejects with an AbortError on the merchant side. This is the pre-existing behaviour.
Rejection with OperationError New in Chrome 149. Treated as an internal payment app error. PaymentRequest.show() rejects with an OperationError on the merchant side, allowing the merchant to distinguish it from user cancellation.

Affected APIs

PaymentRequestEvent.respondWith() On the payment handler side (service worker). Reject the promise argument with new DOMException("…", "OperationError") to signal an internal error.
PaymentRequest.show() On the merchant side. The returned promise now rejects with either AbortError (user cancel) or OperationError (internal payment app error) — previously it always rejected with AbortError.
Source: W3C Payment Request spec.

example

Payment handler (service-worker.js) — signal internal error

self.addEventListener("paymentrequest", (event) => {
  event.respondWith(
    new Promise(async (resolve, reject) => {
      try {
        const result = await openPaymentWindow(event);
        if (result.userCancelled) {
          // User tapped "cancel" — AbortError signals cancellation.
          reject(new DOMException("User cancelled", "AbortError"));
          return;
        }
        resolve({
          methodName: event.methodData[0].supportedMethods,
          details: result.paymentDetails,
        });
      } catch (err) {
        // Internal failure (network error, crypto failure, etc.)
        // OperationError signals an internal payment app error — new in Chrome 149.
        reject(new DOMException("Payment app error: " + err.message, "OperationError"));
      }
    })
  );
});

Merchant page — handle both error types

const request = new PaymentRequest(methodData, details);

try {
  const response = await request.show();
  await processPayment(response);
  await response.complete("success");
} catch (err) {
  if (err.name === "AbortError") {
    // User explicitly cancelled — return to checkout.
    showCheckoutForm();
  } else if (err.name === "OperationError") {
    // Payment app had an internal failure — offer fallback or retry.
    showPaymentError("The payment app encountered an error. Please try again.");
    logToMonitoring("payment_app_internal_error", err.message);
  } else {
    throw err;
  }
}

browser support

Chrome149
Edge149 (Chromium)
FirefoxNo signal (as of Chrome 149)
SafariNo signal (as of Chrome 149)
Source: chromestatus, May 2026.

see also