Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to fix card declined error in Stripe

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.

What you'll learn

  • What Stripe decline codes mean and how to interpret them
  • How to display user-friendly messages for each decline type
  • Why you must never expose fraud-related decline reasons
  • How to handle card_declined errors in your Node.js code
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read15 minutesStripe API v2024-12+, Node.js 18+March 2026RapidDev Engineering Team
TL;DR

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

1

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).

typescript
1// Example error object structure
2{
3 type: 'StripeCardError',
4 code: 'card_declined',
5 decline_code: 'insufficient_funds',
6 message: 'Your card has insufficient funds.',
7 param: undefined
8}

Expected result: You understand the error shape: type, code, decline_code, and message fields.

2

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.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3async function createPayment(amount, currency, paymentMethodId, customerId) {
4 try {
5 const paymentIntent = await stripe.paymentIntents.create({
6 amount: amount, // amount in cents
7 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.

3

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.

typescript
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 user
10 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 fallback
14 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.

4

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.

typescript
1app.post('/create-payment', async (req, res) => {
2 const { amount, currency, paymentMethodId, customerId } = req.body;
3
4 const result = await createPayment(amount, currency, paymentMethodId, customerId);
5
6 if (!result.success) {
7 return res.status(402).json({
8 error: result.userMessage,
9 decline_code: result.declineCode,
10 });
11 }
12
13 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.

5

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.

typescript
1// Test card numbers for common declines (test mode only)
2// 4000000000000002 → generic_decline
3// 4000000000009995 → insufficient_funds
4// 4000000000009987 → lost_card
5// 4000000000009979 → stolen_card
6// 4000000000000069 → expired_card
7// 4000000000000127 → incorrect_cvc
8// 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

payment-handler.js
1require('dotenv').config();
2const express = require('express');
3const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
4
5const app = express();
6app.use(express.json());
7
8function 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}
24
25app.post('/create-payment', async (req, res) => {
26 const { amount, currency, paymentMethodId, customerId } = req.body;
27
28 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 });
40
41 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 }
53
54 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});
61
62const 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.

ChatGPT Prompt

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.

Stripe Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.