w3c / w3c/payment-request

Proposal: Structured Payment Handler Errors via a DOMException-Derived Interface

Open
#1,076 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
HTML
Stars
510
Forks
139
PR merge metrics
No merged PRs in 30d

Description

(Disclaimer: content was generated using AI, then reviewed + heavily edited by smcgruer@).

Introduction

When a payment handler fails to complete a transaction, the merchant site invoking PaymentRequest.show() often needs to understand why the transaction failed so it can respond appropriately (e.g., offering alternative checkout rails, displaying merchant-actionable error messages, or logging incident correlation IDs).

In #1040 we added the ability for payment handlers to indicate internal app error (throwing an OperationError) as a distinct failure state from user cancel (which throws AbortError). However, this does not allow payment handlers any method to communicate details or differentiate between different types of internal app errors.

This proposal introduces a dedicated DOMException-derived interface, PaymentHandlerAppError, allowing payment handlers to communicate structured error reasons and diagnostic metadata to the invoking document via the payment handler indicates an internal error algorithm.


User & Developer Needs

User Need

When a payment app cannot complete a transaction (for example, if the buyer's wallet account is suspended or the payment method is restricted in the merchant's region), the user does not want the checkout process to reset. Instead of assuming the user intentionally abandoned the cart, the merchant site should receive enough context to automatically guide the user to an alternative payment option (e.g., direct card entry, invoice, or buy-now-pay-later).

Developer Need

In payment architectures, a clear boundary exists between issues that can be resolved inside the payment app versus those that require exiting back to the web. Problems that can be corrected within-the-app - such as re-prompting for a PIN/CVV, switching between cards saved in the wallet, or retrying a transient network problem - belong entirely within the payment app’s internal UX.

However, when a payment app encounters an unrecoverable failure and must close, the web layer (the merchant's checkout page or an embedded payment provider SDK) serves as the primary session coordinator. Handing structured context back to the web layer at this point is essential for several key reasons:

  1. Showing the Right Follow-Up to the User:

    • If a user is simply ineligible for a payment method (e.g., unsupported country), the website can smoothly suggest an alternative (like credit card or invoice) without alarming the user.
    • If the payment service itself is down, the website can display a temporary system outage notice or disable the broken button.
  2. Knowing When to Retry vs. When to Stop:

    • Website-side coordination needs to know whether an error is temporary (worth retrying automatically or with a button) or permanent for that session (such as an unsupported account, where retrying will just fail again).
  3. Faster Web Updates vs. Slow App Releases:

    • Websites can deploy changes instantly, whereas native phone apps and OS components often take weeks or months to roll out.
    • Passing error details to the website allows developers to quickly update fallback flows and user messaging without waiting for app-store release cycles.
  4. Accurate Monitoring and Backup Logging:

    • Provides a backup error log directly on the website if the payment app crashes before recording what happened.
    • Lets the website telemetry distinguish expected user drop-offs (like unsupported regions) from real platform crashes, preventing false alarms on engineering dashboards.

Goals & Non-Goals

Goals
  • Differentiate Error Reasons: Allow payment handlers to signal distinct, machine-readable failure reasons.
  • Structured Diagnostic Payloads: Enable payment handlers to attach structured, serializable metadata (e.g., error codes, correlation IDs) to errors.
  • Platform Idiom Consistency: Conform to standard Web Platform error handling patterns using Web IDL's DOMException-derived interface pattern (e.g., RTCError, OverconstrainedError).
  • Backwards Compatibility: Ensure existing implementations and catch blocks checking instanceof DOMException or standard error names do not break.
Non-Goals
  • Standardizing All Business Decline Codes: Individual payment methods define their own payment mechanisms and can define method-specific dictionaries for details.
  • Handling In-App Recoveries: Issues that a payment app can resolve internally (e.g., card selection within the wallet) are out of scope.
  • Passing Non-Serializable Objects: Error details must be serializable across process/origin boundaries.

Key Scenarios & Motivating Use Cases

The following scenarios are adapted from real partner feedback, anonymized.

Scenario 1: User Ineligibility vs. Platform Outages

A website triggers the payment app, but the app determines that the user is not supported for this payment method (e.g., regional compliance restrictions, account eligibility, or missing hardware capabilities).

  • How it works today:
    • The payment app exits and PaymentRequest.show() rejects with a generic OperationError (or AbortError).
    • The problem: The merchant page and client SDK cannot tell whether the user was simply ineligible or whether the payment platform suffered a backend crash, failure, etc.
    • Impact: Ineligible users get treated as system failures, polluting metrics and causing poor fallback behavior.
  • With this proposal:
    • The payment app exits with PaymentHandlerAppError (reason: "user_not_supported"), and the browser passes this through to the merchant page.
    • The improvement: Telemetry records this as expected top-of-funnel attrition. The website silently transitions the user to alternative checkout rails (e.g., credit card or invoice) without displaying a failure message.
Scenario 2: Merchant Configuration Errors

The payment handler validates the transaction request from the merchant and finds an unrecoverable integration error—for example, an expired order token, an invalid merchant account identifier, or an unsupported currency/region combination for that merchant contract.

  • How it works today:
    • The payment app exits and rejects with OperationError.
    • The problem: The merchant SDK has no programmatic way to know that the failure was caused by its own invalid parameters.
    • Impact: Developers often attempt to scrape vendor-specific error.message strings (which breaks across browser versions and localizations) or blindly retry, getting stuck in an error loop. Diagnosing broken merchant integrations requires tedious, manual log-matching across merchant and payment provider support teams.
  • With this proposal:
    • The payment app exits with PaymentHandlerAppError (reason: "merchant_config_error", details: { code: "EXPIRED_ORDER_TOKEN" }), and the browser passes this through to the merchant page.
    • The improvement: The merchant SDK detects the configuration fault, suppresses useless retries, and logs the specific error code to its developer console/telemetry for rapid debugging.
Scenario 3: Rapid UX Iteration

A payment provider wants to experiment with different fallback recovery UIs for users who hit account limits.

  • How it works today:
    • The payment app exits and rejects with OperationError.
  • The problem: Because PaymentRequest.show() only returns OperationError, the website has zero insight into why the wallet closed. Any custom recovery UX or upsell flow must be built entirely inside the native payment app binary.
  • Impact: Native apps/OS wallet modules have multi-week release cycles and multi-month device adoption curves. Product teams cannot quickly test, iterate, or A/B test new fallback experiences on the web.
  • With this proposal:
    • The app exits with PaymentHandlerAppError (reason: "account_restricted", details: { subreason: "spending_limit_reached" }).
    • The improvement: The continuously deployed web SDK on the merchant page receives the structured reason and can immediately experiment with and deploy new recovery UIs directly in the web layer without waiting for native OS rollouts.
Scenario 4: Defense-in-Depth Telemetry

When a fatal internal runtime error occurs in the payment app, the app's internal logging mechanism may fail or be cut short if the worker or container terminates abruptly.

  • How it works today:
    • The app crashes or terminates abruptly, potentially before its own internal telemetry pings can be dispatched over the network. PaymentRequest.show() rejects with OperationError (sent by the app as it closed).
    • The problem: The payment provider has a complete telemetry blind spot for these fatal client-side crashes.
    • Impact: The merchant site only sees a generic failure with no correlation ID or trace token, making it impossible for engineering teams to correlate client-side crashes with backend server logs.
  • With this proposal:
    • The payment app attaches diagnostic metadata upon exit (details: { correlationId: "txn_trace_849204", failureStage: "TOKENIZATION", errorCode: "INTERNAL_STATE_CORRUPT" }).
    • The improvement: The provider's client-side SDK embedded on the merchant page catches the error and sends the correlation ID to its observability pipeline, serving as a reliable backup telemetry channel.

Proposed Design

Introduce a new PaymentHandlerAppError type that can be raised by Payment Handlers with dedicated reason and details attributes. Update the payment handler indicates an internal error algorithm to optionally accept structured failure information from the payment handler. If provided, the user agent will reject the PaymentRequest.show() promise with an instance of PaymentHandlerAppError carrying the supplied reason and details payload.

For backwards compatibility, the specification would continue to support existing error pathways: if a payment handler indicates an internal error without structured metadata, PaymentRequest.show() will continue to reject with a generic OperationError, while user-initiated cancellations will continue to reject with AbortError.

Web IDL Definition

Following the Web IDL DOMException-derived interface pattern:

dictionary PaymentHandlerAppErrorInit {
  DOMString reason;
  object details;
};

[Exposed=(Window,Worker), Serializable]
interface PaymentHandlerAppError : DOMException {
  constructor(optional DOMString message = "", optional PaymentHandlerAppErrorInit options = {});

  readonly attribute DOMString? reason;
  readonly attribute object? details;
};
Code Walkthrough
1. Web-based Payment Handler (example)

When indicating an internal error, the payment handler supplies the structured error data:

// In the payment handler service worker:
self.addEventListener("paymentrequest", (event) => {
  event.respondWith(
    processPayment(event).catch((err) => {
      // Reject with structured PaymentHandlerAppError
      throw new PaymentHandlerAppError("User is not eligible for this payment method", {
        reason: "user_not_supported",
        details: {
          code: "REGIONAL_INELIGIBILITY",
          correlationId: "txn_err_98234"
        }
      });
    })
  );
});
2. Merchant website
const request = new PaymentRequest(methods, details, options);

try {
  const response = await request.show();
  await response.complete("success");
} catch (err) {
  if (err instanceof PaymentHandlerAppError) {
    // Handle structured payment handler error
    paymentProviderSDK.logTelemetry("payment_handler_failure", {
      reason: err.reason,
      details: err.details,
      message: err.message
    });

   // 2. Actionable merchant-side UI handling
    switch (err.reason) {
      case "account_restricted":
      case "user_not_supported":
        showAlternativePaymentMethods();
        break;
      case "merchant_config_error":
       logIntegrationError(err.details);
        showGenericFallbackCheckout();
        break;
      default:
        displayErrorMessage(err.message);
    }
  } else if (err.name === "AbortError") {
    // User intentionally closed or cancelled the payment UI
    console.log("User cancelled checkout");
  } else {
    // Other browser/DOM errors
    console.error("Unexpected error during PaymentRequest:", err);
  }
}

Alternatives Considered

Approach Description Pros Cons
Option 1: DOMException-Derived Interface (PaymentHandlerAppError) (Recommended) Introduce a new interface subclassing DOMException. • Follows platform precedent (RTCError, OverconstrainedError)
• Clean instanceof ergonomics
• Fully backwards compatible
• Introduces a new Web IDL interface
Option 2: Support standard DOMException names Allow handlers to reject with any standard DOMException name (NetworkError, TimeoutError, etc.). • No new interface required • Cannot carry structured debugging metadata or decline codes
• Origin of errors is ambiguous
• Restricted to concepts which have an existing DOMException
Option 3: Structured Error in PaymentResponse.details Resolve the promise with { error: { code, data } } rather than rejecting. • Reuses existing response dictionary passing
• Can be done by payment apps today with zero spec or browser changes
• Breaks standard JS Promise conventions (merchants expect show() to reject on failure, not resolve)

Security & Privacy Considerations

This proposal introduces no new security or privacy surface area beyond what is already established by the Payment Request and Payment Handler specifications:

  1. Existing Data-Sharing Model: When a payment completes successfully, the payment handler already returns arbitrary structured data to the merchant origin via PaymentHandlerResponse.details. Returning structured error metadata in PaymentHandlerAppError.details operates within the exact same trust boundary—the payment app remains in full control over what data it provides to the merchant.

  2. Standard Serialization: The user agent already serializes details across the Service Worker / renderer boundary using structured clone algorithms. Reusing this existing pipeline for PaymentHandlerAppError.details introduces no new IPC or memory-safety risks.


Open Questions

  1. Reason Classification: Should reason be an open DOMString or a standardized extensible enum (e.g. "user_not_supported", "merchant_config_error", "other")?
  2. Standard Error Forwarding: Should we also allow handlers to reject with standard DOMException types (e.g., TimeoutError) as well, or just the new PaymentHandlerAppError and legacy OperationError ?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue names no repository files or tests. Start by reading the payment handler indicates an internal error algorithm and the linked Web IDL DOMException-derived interface pattern; done means agreeing on and specifying the new error interface, structured data, and backwards-compatible error flow.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
api, payments, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.