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 |
Status disagreement, both sides reported rather than resolved (fetched 2026-10-07): this page records the feature as "Shipped in Chrome 149 / Edge 149" with status "Enabled by default", while the ChromeStatus record 5942637229113344 reports a summary status of Proposed with is_released false (accurate as of 2026-05-25), though its ship stage still carries desktop, Android, and WebView milestone 149. Neither side is adjudicated here.
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. |
Behavior disagreement, both sides reported rather than resolved (fetched 2026-10-07): this page documents that PaymentRequest.show() rejects with OperationError for an internal payment app error and with AbortError for user cancellation, while the MDN PaymentRequest.show() reference lists AbortError, InvalidStateError, NotSupportedError, and SecurityError as its exception types, with no OperationError case. Neither side is adjudicated here.
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) |
Browser-support disagreement, both sides reported rather than resolved (fetched 2026-10-07): this page lists Firefox and Safari as No signal, while the ChromeStatus record 5942637229113344 reports the Firefox view as N/A with the note "Firefox does not ship Payment Request or Payment Handler" and the Safari view as Positive. Neither side is adjudicated here.