card_declined in Checkout: the error you must not retry, the state you must show, and why the customer saw the payment fail twice
Problem
An order flow where declined cards caused chaos: support tickets saying "you charged me twice," and logs showing the same PaymentIntent attempted five times in a row:
StripeCardError: Your card was declined.
code: card_declined
decline_code: generic_decline
payment_intent: pi_3Pxyz...
(status 402)The retry loop was written by someone reading "handle transient errors" and treating 402 as transient. It is not.
Root cause
card_declined is a terminal answer from the issuing bank about this exact attempt — the card said no. Retrying the identical request either keeps failing (real decline) or creates the worst case: insufficient_funds declines, the customer tops up, and the automatic retry succeeds hours later for an order the customer assumed was dead — hence "you charged me twice."
A 402 needs three distinct behaviors, not a retry policy: a user-facing message derived from decline_code, session state that records the failure, and a fresh attempt only when the user actively tries again. Complicating this: some declines are recoverable in a different sense — authentication_required means the PaymentIntent now has a next_action the customer must complete (3DS), and treating that as a plain failure loses the sale.
async function confirmPayment(paymentIntentId) {
try {
return await stripe.paymentIntents.confirm(paymentIntentId);
… 22 more lines in the fix🔒 the fix — including 2 code blocks — is members-only. $1/mo unlocks everything.