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
Use the generic decline test card
Use the generic decline test card
Card number 4000000000000002 simulates a generic card decline. Use it to test your basic error handling.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23try {4 const paymentIntent = await stripe.paymentIntents.create({5 amount: 2000, // $20.006 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.
Test specific decline reasons
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.
1// Insufficient funds2// Card: 40000000000099953// pm token: pm_card_chargeDeclinedInsufficientFunds45// Expired card6// Card: 40000000000000697// pm token: pm_card_chargeDeclinedExpiredCard89// Incorrect CVC10// Card: 400000000000012711// pm token: pm_card_chargeDeclinedIncorrectCvc1213// Processing error14// Card: 400000000000011915// pm token: pm_card_chargeDeclinedProcessingError1617// Lost card18// Card: 40000000000099871920// Stolen card21// Card: 4000000000009979Expected result: Each test card produces a different decline_code (insufficient_funds, expired_card, incorrect_cvc, processing_error).
Handle decline errors in your code
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.
1function getDeclineMessage(err) {2 if (err.type !== 'StripeCardError') {3 return 'An unexpected error occurred. Please try again.';4 }56 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 };1516 return messages[err.decline_code] || messages.card_declined;17}Expected result: Your app shows a clear, specific message for each type of decline.
Test 3D Secure failure
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.
1// Card: 4000008400001629 triggers 3DS that always fails2// Card: 4000000000003220 triggers 3DS that can succeed or fail34const paymentIntent = await stripe.paymentIntents.create({5 amount: 3000, // $30.006 currency: 'usd',7 payment_method: 'pm_card_authenticationRequiredChargeDeclinedInsufficientFunds',8 confirm: true,9 return_url: 'https://yoursite.com/payment-result',10});1112console.log('Status:', paymentIntent.status);13// 'requires_action' — customer must complete 3DSExpected result: The PaymentIntent has status requires_action. After the customer fails 3DS authentication, the status changes to requires_payment_method.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();5app.use(express.json());67// Map decline codes to user-friendly messages8const 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};1718app.post('/api/pay', async (req, res) => {19 try {20 const { paymentMethodId, amount } = req.body;2122 const paymentIntent = await stripe.paymentIntents.create({23 amount, // in cents24 currency: 'usd',25 payment_method: paymentMethodId,26 confirm: true,27 automatic_payment_methods: {28 enabled: true,29 allow_redirects: 'never',30 },31 });3233 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});4546const 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation