Card declined errors in Stripe come with specific decline codes like insufficient_funds, lost_card, or do_not_honor. Map each decline code to a user-friendly message, never expose fraud-related decline reasons to the cardholder, and prompt users to try a different payment method or contact their bank. Handle the error in your code by catching StripeCardError exceptions.
Understanding Card Declined Errors in Stripe
When a payment fails, Stripe returns a card_declined error with a specific decline_code. These codes tell you exactly why the card was rejected — insufficient funds, expired card, incorrect CVC, or fraud suspicion. Your job is to translate these into helpful messages for the customer while keeping sensitive fraud information private. Properly handling declines improves conversion rates and reduces support tickets.
Prerequisites
- A Stripe account in test mode
- Node.js 18+ and the Stripe npm package installed
- Basic understanding of try/catch error handling in JavaScript
Step-by-step guide
Understand the error structure
Understand the error structure
When a card is declined, Stripe throws an error with type 'StripeCardError'. The error object contains err.code (always 'card_declined'), err.decline_code (the specific reason), and err.message (Stripe's default message).
1// Example error object structure2{3 type: 'StripeCardError',4 code: 'card_declined',5 decline_code: 'insufficient_funds',6 message: 'Your card has insufficient funds.',7 param: undefined8}Expected result: You understand the error shape: type, code, decline_code, and message fields.
Catch the card declined error in your payment code
Catch the card declined error in your payment code
Wrap your payment creation in a try/catch block and check for the card_declined code. Extract the decline_code to determine the specific reason.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23async function createPayment(amount, currency, paymentMethodId, customerId) {4 try {5 const paymentIntent = await stripe.paymentIntents.create({6 amount: amount, // amount in cents7 currency: currency,8 customer: customerId,9 payment_method: paymentMethodId,10 confirm: true,11 automatic_payment_methods: {12 enabled: true,13 allow_redirects: 'never',14 },15 });16 return { success: true, paymentIntent };17 } catch (err) {18 if (err.code === 'card_declined') {19 return {20 success: false,21 declineCode: err.decline_code,22 userMessage: getUserMessage(err.decline_code),23 };24 }25 throw err;26 }27}Expected result: Your payment function catches card_declined errors and returns a structured response with the decline code and user-friendly message.
Map decline codes to user-friendly messages
Map decline codes to user-friendly messages
Create a mapping of decline codes to messages that are helpful without exposing sensitive information. Never tell users their card was flagged for fraud — instead, suggest contacting their bank.
1function getUserMessage(declineCode) {2 const messages = {3 insufficient_funds: 'Your card has insufficient funds. Please try a different card.',4 card_not_supported: 'Your card does not support this type of purchase. Please try a different card.',5 expired_card: 'Your card has expired. Please update your payment method.',6 incorrect_cvc: 'The CVC number is incorrect. Please check and try again.',7 incorrect_number: 'The card number is incorrect. Please check and try again.',8 processing_error: 'An error occurred while processing your card. Please try again.',9 // NEVER expose these fraud-related reasons to the user10 fraudulent: 'Your card was declined. Please contact your bank or try a different card.',11 lost_card: 'Your card was declined. Please contact your bank or try a different card.',12 stolen_card: 'Your card was declined. Please contact your bank or try a different card.',13 // Generic fallback14 do_not_honor: 'Your card was declined. Please contact your bank or try a different card.',15 generic_decline: 'Your card was declined. Please try a different payment method.',16 };17 return messages[declineCode] || 'Your card was declined. Please try a different payment method or contact your bank.';18}Expected result: Each decline code maps to a user-friendly message. Fraud codes show a generic bank contact message.
Return the error to your frontend
Return the error to your frontend
In your Express route, catch the decline and send the appropriate message back to the client. The frontend should display this message near the payment form.
1app.post('/create-payment', async (req, res) => {2 const { amount, currency, paymentMethodId, customerId } = req.body;34 const result = await createPayment(amount, currency, paymentMethodId, customerId);56 if (!result.success) {7 return res.status(402).json({8 error: result.userMessage,9 decline_code: result.declineCode,10 });11 }1213 res.json({ clientSecret: result.paymentIntent.client_secret });14});Expected result: The API returns a 402 status with a user-friendly error message when a card is declined.
Test with Stripe test cards
Test with Stripe test cards
Stripe provides test card numbers that trigger specific decline codes. Use these in test mode to verify your error handling works correctly for each decline scenario.
1// Test card numbers for common declines (test mode only)2// 4000000000000002 → generic_decline3// 4000000000009995 → insufficient_funds4// 4000000000009987 → lost_card5// 4000000000009979 → stolen_card6// 4000000000000069 → expired_card7// 4000000000000127 → incorrect_cvc8// 4242424242424242 → Always succeeds (use for happy path)Expected result: Each test card triggers the corresponding decline code. Your error handler returns the correct user-friendly message for each.
Complete working example
1require('dotenv').config();2const express = require('express');3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);45const app = express();6app.use(express.json());78function getUserMessage(declineCode) {9 const messages = {10 insufficient_funds: 'Your card has insufficient funds. Please try a different card.',11 card_not_supported: 'Your card does not support this type of purchase.',12 expired_card: 'Your card has expired. Please update your payment method.',13 incorrect_cvc: 'The CVC number is incorrect. Please check and try again.',14 incorrect_number: 'The card number is incorrect. Please check and try again.',15 processing_error: 'An error occurred processing your card. Please try again.',16 fraudulent: 'Your card was declined. Please contact your bank.',17 lost_card: 'Your card was declined. Please contact your bank.',18 stolen_card: 'Your card was declined. Please contact your bank.',19 do_not_honor: 'Your card was declined. Please contact your bank.',20 generic_decline: 'Your card was declined. Please try a different payment method.',21 };22 return messages[declineCode] || 'Your card was declined. Please try a different payment method.';23}2425app.post('/create-payment', async (req, res) => {26 const { amount, currency, paymentMethodId, customerId } = req.body;2728 try {29 const paymentIntent = await stripe.paymentIntents.create({30 amount,31 currency: currency || 'usd',32 customer: customerId,33 payment_method: paymentMethodId,34 confirm: true,35 automatic_payment_methods: {36 enabled: true,37 allow_redirects: 'never',38 },39 });4041 res.json({42 success: true,43 clientSecret: paymentIntent.client_secret,44 });45 } catch (err) {46 if (err.code === 'card_declined') {47 return res.status(402).json({48 success: false,49 error: getUserMessage(err.decline_code),50 decline_code: err.decline_code,51 });52 }5354 console.error('Payment error:', err.message);55 res.status(500).json({56 success: false,57 error: 'An unexpected error occurred. Please try again.',58 });59 }60});6162const PORT = process.env.PORT || 3000;63app.listen(PORT, () => console.log(`Payment server running on port ${PORT}`));Common mistakes when fixing card declined error in Stripe
Why it's a problem: Exposing fraud decline reasons (fraudulent, lost_card, stolen_card) to the customer
How to avoid: Always show a generic 'contact your bank' message for fraud-related declines. Exposing fraud detection helps fraudsters refine their techniques.
Why it's a problem: Showing Stripe's raw error message directly to the user
How to avoid: Map decline_code values to your own user-friendly messages. Stripe's messages are technical and not ideal for end users.
Why it's a problem: Not handling the card_declined error type specifically
How to avoid: Check for err.code === 'card_declined' and extract err.decline_code. A generic error catch loses the valuable decline reason information.
Why it's a problem: Using test card 4242424242424242 to test declines
How to avoid: 4242424242424242 always succeeds. Use the specific decline test cards: 4000000000000002 (generic), 4000000000009995 (insufficient funds), etc.
Best practices
- Map every decline code to a clear, non-technical message the customer can act on
- Never expose fraud-related decline reasons — always use generic 'contact your bank' messaging
- Log the full decline_code server-side for debugging while showing friendly messages to users
- Offer alternative payment methods when a card is declined
- Use Stripe test cards to verify error handling for every decline scenario
- Return a 402 HTTP status for payment failures so your frontend can handle them distinctly
- Consider implementing a retry prompt for transient declines like processing_error
- Track decline rates by code in your analytics to identify patterns
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express endpoint that creates a Stripe PaymentIntent and handles card_declined errors. Map each decline_code to a user-friendly message. Never expose fraud-related decline reasons (fraudulent, lost_card, stolen_card) to the user — show a generic 'contact your bank' message instead.
Create a Stripe payment handler in Node.js that catches card_declined errors with specific decline codes. Include a function that maps decline codes to user-friendly messages, hiding fraud-related codes behind generic messages. Return 402 status for declines.
Frequently asked questions
What does the card_declined error mean in Stripe?
It means the cardholder's bank rejected the charge. The specific reason is in the decline_code field — it could be insufficient funds, expired card, incorrect CVC, or a bank-side fraud block.
Should I show the Stripe decline code to my users?
No. Map the decline_code to a user-friendly message. For fraud-related codes (fraudulent, lost_card, stolen_card), always show a generic 'contact your bank' message to avoid tipping off bad actors.
What is the do_not_honor decline code?
do_not_honor is a generic decline from the bank. It means the bank rejected the charge without providing a specific reason. The customer should contact their bank for details or try a different card.
How do I test card declined errors in Stripe?
Use Stripe test cards in test mode: 4000000000000002 triggers generic_decline, 4000000000009995 triggers insufficient_funds, 4000000000000069 triggers expired_card. Card 4242424242424242 always succeeds.
Can I retry a declined card payment?
Yes, but only for transient declines like processing_error. For declines like insufficient_funds or expired_card, the customer needs to resolve the issue with their bank or use a different card before retrying.
What HTTP status code should I return for card declined errors?
Return HTTP 402 Payment Required. This clearly signals a payment issue to your frontend, making it easy to show the appropriate error UI.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation