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 in | Chrome 149 / Edge 149 |
|---|---|
| Status | Enabled by default |
| Flag | None |
| Spec | W3C Payment Request — §"Payment handler indicates an internal error" |
| ChromeStatus | 5942637229113344 — 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. |
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
| Chrome | 149 |
|---|---|
| Edge | 149 (Chromium) |
| Firefox | No signal (as of Chrome 149) |
| Safari | No signal (as of Chrome 149) |