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

How to test failed payments in Stripe

Test failed payments in Stripe using special test card numbers that simulate declines. Card 4000000000000002 triggers a generic decline, 4000000000009995 simulates insufficient funds, and 4000000000000069 triggers an expired card error. Always test failure paths in test mode before going live to ensure your app handles errors gracefully.

What you'll learn

  • How to use Stripe's test decline cards to simulate payment failures
  • The most common decline codes and what they mean
  • How to test 3D Secure authentication failures
  • How to handle failed payment responses in your code
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read15 minutesStripe API v2024-12+, Node.js 18+, Stripe test modeMarch 2026RapidDev Engineering Team
TL;DR

Test failed payments in Stripe using special test card numbers that simulate declines. Card 4000000000000002 triggers a generic decline, 4000000000009995 simulates insufficient funds, and 4000000000000069 triggers an expired card error. Always test failure paths in test mode before going live to ensure your app handles errors gracefully.

Why Test Failed Payments?

Most developers only test the happy path — successful payments. But in production, 5-15% of payments fail due to insufficient funds, expired cards, fraud blocks, or network errors. If your app does not handle these failures gracefully, customers see confusing errors or get stuck on broken checkout pages. Stripe provides a set of test card numbers that simulate every type of decline so you can build proper error handling before going live.

Prerequisites

  • A Stripe account in test mode (toggle in the Dashboard)
  • Node.js 18 or newer installed
  • The stripe npm package installed (npm install stripe)
  • Your test secret key (sk_test_...) from Dashboard → Developers → API keys

Step-by-step guide

1

Use the generic decline test card

Card number 4000000000000002 simulates a generic card decline. Use it to test your basic error handling.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3try {
4 const paymentIntent = await stripe.paymentIntents.create({
5 amount: 2000, // $20.00
6 currency: 'usd',
7 payment_method: 'pm_card_chargeDeclined',
8 confirm: true,
9 automatic_payment_methods: {
10 enabled: true,
11 allow_redirects: 'never',
12 },
13 });
14} catch (err) {
15 console.log('Decline code:', err.code);
16 console.log('Message:', err.message);
17 // err.code === 'card_declined'
18}

Expected result: The API throws a card_declined error with a 402 status code.

2

Test specific decline reasons

Use different test cards to simulate specific failure scenarios. Each card triggers a different decline_code that you can use to show the customer a helpful message.

typescript
1// Insufficient funds
2// Card: 4000000000009995
3// pm token: pm_card_chargeDeclinedInsufficientFunds
4
5// Expired card
6// Card: 4000000000000069
7// pm token: pm_card_chargeDeclinedExpiredCard
8
9// Incorrect CVC
10// Card: 4000000000000127
11// pm token: pm_card_chargeDeclinedIncorrectCvc
12
13// Processing error
14// Card: 4000000000000119
15// pm token: pm_card_chargeDeclinedProcessingError
16
17// Lost card
18// Card: 4000000000009987
19
20// Stolen card
21// Card: 4000000000009979

Expected result: Each test card produces a different decline_code (insufficient_funds, expired_card, incorrect_cvc, processing_error).

3

Handle decline errors in your code

Catch Stripe errors and map decline codes to user-friendly messages. Display these messages in your checkout UI instead of raw error text.

typescript
1function getDeclineMessage(err) {
2 if (err.type !== 'StripeCardError') {
3 return 'An unexpected error occurred. Please try again.';
4 }
5
6 const messages = {
7 card_declined: 'Your card was declined. Please try a different card.',
8 insufficient_funds: 'Insufficient funds. Please try a different card or contact your bank.',
9 expired_card: 'Your card has expired. Please use a different card.',
10 incorrect_cvc: 'The CVC number is incorrect. Please check and try again.',
11 processing_error: 'A processing error occurred. Please try again in a moment.',
12 lost_card: 'This card has been reported lost. Please use a different card.',
13 stolen_card: 'This card has been reported stolen. Please use a different card.',
14 };
15
16 return messages[err.decline_code] || messages.card_declined;
17}

Expected result: Your app shows a clear, specific message for each type of decline.

4

Test 3D Secure failure

Card 4000008400001629 triggers a 3D Secure authentication that the customer fails. This tests your handling of requires_action status followed by authentication failure.

typescript
1// Card: 4000008400001629 triggers 3DS that always fails
2// Card: 4000000000003220 triggers 3DS that can succeed or fail
3
4const paymentIntent = await stripe.paymentIntents.create({
5 amount: 3000, // $30.00
6 currency: 'usd',
7 payment_method: 'pm_card_authenticationRequiredChargeDeclinedInsufficientFunds',
8 confirm: true,
9 return_url: 'https://yoursite.com/payment-result',
10});
11
12console.log('Status:', paymentIntent.status);
13// 'requires_action' — customer must complete 3DS

Expected result: The PaymentIntent has status requires_action. After the customer fails 3DS authentication, the status changes to requires_payment_method.

Complete working example

test-failures.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5app.use(express.json());
6
7// Map decline codes to user-friendly messages
8const DECLINE_MESSAGES = {
9 card_declined: 'Your card was declined. Please try a different card.',
10 insufficient_funds: 'Insufficient funds. Please try another payment method.',
11 expired_card: 'Your card has expired. Please update your card details.',
12 incorrect_cvc: 'Incorrect CVC. Please check the number on the back of your card.',
13 processing_error: 'A processing error occurred. Please try again.',
14 lost_card: 'This card cannot be used. Please try a different card.',
15 stolen_card: 'This card cannot be used. Please try a different card.',
16};
17
18app.post('/api/pay', async (req, res) => {
19 try {
20 const { paymentMethodId, amount } = req.body;
21
22 const paymentIntent = await stripe.paymentIntents.create({
23 amount, // in cents
24 currency: 'usd',
25 payment_method: paymentMethodId,
26 confirm: true,
27 automatic_payment_methods: {
28 enabled: true,
29 allow_redirects: 'never',
30 },
31 });
32
33 res.json({ status: paymentIntent.status, id: paymentIntent.id });
34 } catch (err) {
35 if (err.type === 'StripeCardError') {
36 const message = DECLINE_MESSAGES[err.decline_code] || DECLINE_MESSAGES.card_declined;
37 return res.status(402).json({
38 error: message,
39 decline_code: err.decline_code,
40 });
41 }
42 res.status(500).json({ error: 'An unexpected error occurred.' });
43 }
44});
45
46const PORT = process.env.PORT || 3000;
47app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Common mistakes when testing failed payments in Stripe

Why it's a problem: Only testing with 4242424242424242 (the success card)

How to avoid: Test with decline cards too. In production, a significant percentage of charges fail. Your app must handle each decline type gracefully.

Why it's a problem: Showing raw Stripe error messages to customers

How to avoid: Map decline_code values to friendly, non-technical messages. Never expose internal error details or Stripe error IDs to end users.

Why it's a problem: Not testing 3D Secure failures

How to avoid: Use card 4000008400001629 to test 3DS authentication failure. Many European cards require 3DS, so this path must work.

Why it's a problem: Using test cards in live mode

How to avoid: Test card numbers only work with test API keys (sk_test_). They are rejected in live mode. Always verify you are in test mode.

Best practices

  • Test every decline code your app might encounter: insufficient funds, expired card, incorrect CVC, and processing errors
  • Map each decline_code to a user-friendly message — never show raw API errors to customers
  • Test 3D Secure flows with cards 4000000000003220 and 4000008400001629
  • Verify that your frontend gracefully handles error responses and lets customers retry with a different card
  • Test with different amounts — some test behaviors are amount-dependent
  • Log decline codes and rates to monitor payment health in production
  • Use Stripe's test clocks to simulate subscription payment failures over time
  • Test the full flow end-to-end: decline → error message → retry → success

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express payment endpoint that handles Stripe card declines gracefully. Map decline codes (insufficient_funds, expired_card, incorrect_cvc, etc.) to user-friendly error messages. Return appropriate HTTP status codes for each error type.

Stripe Prompt

Add error handling to my Stripe payment flow. When a card is declined, catch the error, check the decline_code, and return a specific user-friendly message. Include test instructions using Stripe's decline test cards.

Frequently asked questions

Where can I find a full list of Stripe test card numbers?

Visit docs.stripe.com/testing. The page lists all test cards for declines, 3D Secure, different card brands, different countries, and specific error scenarios.

Do test declines affect my account standing?

No. Test mode transactions are completely separate from live mode. Declines in test mode do not affect your account health, dispute rate, or any metrics.

Can I trigger a specific error amount in test mode?

Some test behaviors are triggered by card number, not amount. However, for certain error simulations like partial captures, the amount matters. Check Stripe's testing documentation for amount-specific behaviors.

How do I test webhook events for failed payments?

Use the Stripe CLI with 'stripe trigger payment_intent.payment_failed' to send test webhook events to your local server, or create actual test declines and let the webhook fire naturally.

What if I need help building a robust payment error handling system?

For applications that need retry logic, smart routing, dunning flows, and comprehensive decline analytics, the RapidDev team can help architect a production-grade payment system.

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.