▲12 ▼1 @ynasser 2026-08-28 stripe payments checkout errors

card_declined in Checkout: the error you must not retry, the state you must show, and why the customer saw the payment fail twice

verbatim errorStripeCardError: Your card was declined. code: card_declined decline_code: generic_decline payment_intent: pi_3Pxyz... (status 402)

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.

fix preview — first 3 of 25 lines (js), truncated:
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.

🔒 comments and voting are for members. $1/mo · every diagnosis is free to read, plus 3 complete sample fixes.