As identified in #1040, the spec has an over reliance on AbortError...
The Payment Request specification uses AbortError inconsistently across different failure scenarios, conflating:
- User-initiated cancellation
- Developer errors (API misuse)
- Security/policy violations
- System/infrastructure failures
This makes it impossible for merchants to programmatically distinguish between these fundamentally different error categories.
Error Categories (Web Platform Conventions)
| Category |
Appropriate Exception |
Semantics |
| Developer error |
InvalidStateError, NotAllowedError |
API called incorrectly; caller's fault |
| User action |
AbortError |
User explicitly cancelled |
| Security/policy |
SecurityError, NotAllowedError |
Permission denied, policy violation |
| System/infra |
OperationError |
External system failure (handler crash, OS kill) |
Problematic Cases in show()
1. Document Not Visible
Location: show() method, step 6
Current behavior:
If document's visibility state is not "visible", return a promise
rejected with an "AbortError" DOMException.
Problem: This is a developer error — the page called show() while not visible (e.g., background tab). The user didn't abort anything.
Should be: NotAllowedError
2. Payment Request Already Showing
Location: show() method, step 8
Current behavior:
If the user agent's payment request is showing boolean is true, then:
- Set request.[[state]] to "closed"
- Return a promise rejected with an "AbortError" DOMException.
Problem: This is a developer error — calling show() when a payment UI is already visible. The user didn't abort anything.
Should be: InvalidStateError
Rationale:
- This is classic "object in invalid state" per WebIDL
- Similar to calling
start() on an already-started operation
- Consistent with step 7 which uses
InvalidStateError for wrong [[state]]
3. UA Immediate Abort (Private Browsing)
Location: show() method, step 12
Current behavior:
Optionally:
- Reject acceptPromise with an "AbortError" DOMException.
- [Note: allows UA to act as if user immediately aborted]
Problem: This conflates UA policy decisions with user cancellation. In private browsing mode, the user agent is refusing, not the user.
Should be: NotAllowedError
Rationale:
- Private browsing = UA policy to protect privacy =
NotAllowedError
4. detailsPromise Rejection
Location: Update algorithm
Current behavior:
Upon rejection of detailsPromise:
- Abort the update with request and an "AbortError" DOMException.
Problem: If the merchant's detailsPromise rejects, that's a developer error (their server failed, their code threw). Using AbortError implies the user cancelled.
Should be: Pass through the original rejection reason, or use OperationError
Rationale:
- The merchant's own promise failed; that's not "abort".
- Original exception provides better debugging
Problematic Cases in retry()
5. Document Becomes Inactive During Retry
Location: retry() method
Current behavior:
If document stops being fully active while the user interface is being shown:
- Close down the user interface
- [No explicit rejection specified, but likely AbortError]
Problem: Document deactivation during retry is an infrastructure failure, not user cancellation.
Should be: InvalidStateError
Proposed Fix Summary
| Case |
Current |
Proposed |
Priority |
| Document not visible |
AbortError |
InvalidStateError |
High |
| Already showing |
AbortError |
InvalidStateError |
High |
| Private browsing abort |
AbortError |
NotAllowedError |
Medium |
| detailsPromise rejects |
AbortError |
Original error / OperationError |
High |
| Payment handler error |
AbortError |
OperationError |
High |
Backwards Compatibility
Changing error types is technically breaking, but:
- Most merchants catch all errors from
show() generically
- Those parsing error messages are already dealing with inconsistency
- Semantic correctness enables better error handling going forward
- Can be staged: update spec, then implementations, with clear release notes
As identified in #1040, the spec has an over reliance on AbortError...
The Payment Request specification uses
AbortErrorinconsistently across different failure scenarios, conflating:This makes it impossible for merchants to programmatically distinguish between these fundamentally different error categories.
Error Categories (Web Platform Conventions)
InvalidStateError,NotAllowedErrorAbortErrorSecurityError,NotAllowedErrorOperationErrorProblematic Cases in
show()1. Document Not Visible
Location:
show()method, step 6Current behavior:
Problem: This is a developer error — the page called
show()while not visible (e.g., background tab). The user didn't abort anything.Should be:
NotAllowedError2. Payment Request Already Showing
Location:
show()method, step 8Current behavior:
Problem: This is a developer error — calling
show()when a payment UI is already visible. The user didn't abort anything.Should be:
InvalidStateErrorRationale:
start()on an already-started operationInvalidStateErrorfor wrong[[state]]3. UA Immediate Abort (Private Browsing)
Location:
show()method, step 12Current behavior:
Problem: This conflates UA policy decisions with user cancellation. In private browsing mode, the user agent is refusing, not the user.
Should be:
NotAllowedErrorRationale:
NotAllowedError4. detailsPromise Rejection
Location: Update algorithm
Current behavior:
Problem: If the merchant's
detailsPromiserejects, that's a developer error (their server failed, their code threw). UsingAbortErrorimplies the user cancelled.Should be: Pass through the original rejection reason, or use
OperationErrorRationale:
Problematic Cases in
retry()5. Document Becomes Inactive During Retry
Location:
retry()methodCurrent behavior:
Problem: Document deactivation during retry is an infrastructure failure, not user cancellation.
Should be:
InvalidStateErrorProposed Fix Summary
AbortErrorInvalidStateErrorAbortErrorInvalidStateErrorAbortErrorNotAllowedErrorAbortErrorOperationErrorAbortErrorOperationErrorBackwards Compatibility
Changing error types is technically breaking, but:
show()generically