Stripe payment failures can stem from card declines, authentication requirements, invalid parameters, or network issues. Debug them by checking the PaymentIntent status, reading the last_payment_error object, inspecting the Stripe Dashboard event logs, and handling each failure type in your code. Most failures are resolved by displaying clear messages and letting customers retry with corrected information.
Debugging Stripe Payment Failures Step by Step
When a Stripe payment fails, the PaymentIntent moves to a 'requires_payment_method' or 'requires_action' status instead of 'succeeded'. The last_payment_error field contains the failure details — decline code, error type, and a human-readable message. Understanding these statuses and error types lets you display helpful messages to the customer and log the details for your team. This guide covers the full debugging workflow from API error to resolution.
Prerequisites
- A Stripe account in test mode
- Node.js 18+ with the Stripe npm package
- Access to the Stripe Dashboard for event log inspection
- A payment form using Stripe.js and Elements
Step-by-step guide
Check the PaymentIntent status
Check the PaymentIntent status
Retrieve the PaymentIntent and check its status. Common statuses after failure: 'requires_payment_method' (card declined or invalid), 'requires_action' (3D Secure needed), 'canceled' (explicitly canceled), 'processing' (still in progress).
1const paymentIntent = await stripe.paymentIntents.retrieve('pi_abc123');2console.log('Status:', paymentIntent.status);3console.log('Error:', paymentIntent.last_payment_error);Expected result: You can see the PaymentIntent status and the specific error that caused the failure.
Read the last_payment_error object
Read the last_payment_error object
The last_payment_error field contains all the details you need: type (card_error, invalid_request_error, etc.), code (card_declined, expired_card, etc.), decline_code (insufficient_funds, etc.), and message.
1const error = paymentIntent.last_payment_error;2if (error) {3 console.log('Error type:', error.type);4 console.log('Error code:', error.code);5 console.log('Decline code:', error.decline_code);6 console.log('Message:', error.message);7}Expected result: The error details tell you exactly why the payment failed — card decline, authentication, or API issue.
Check the Stripe Dashboard event logs
Check the Stripe Dashboard event logs
Go to Stripe Dashboard → Developers → Events. Filter by 'payment_intent.payment_failed'. Click an event to see the full error details, timeline, and any associated requests. This is the fastest way to debug production payment failures.
Expected result: The Dashboard shows the complete event history for the failed payment, including the error response Stripe sent.
Handle 3D Secure authentication requirements
Handle 3D Secure authentication requirements
If the status is 'requires_action', the customer's bank requires 3D Secure authentication. Use stripe.confirmPayment() on the frontend with Stripe.js to trigger the authentication flow automatically.
1// Frontend — Stripe.js handles 3D Secure automatically2const { error, paymentIntent } = await stripe.confirmPayment({3 elements,4 confirmParams: {5 return_url: 'https://yoursite.com/payment/complete',6 },7});89if (error) {10 // Authentication failed or was abandoned11 console.log('Payment failed:', error.message);12} else if (paymentIntent.status === 'succeeded') {13 console.log('Payment succeeded after 3D Secure');14}Expected result: Stripe.js opens the 3D Secure authentication modal. After the customer completes authentication, the payment either succeeds or fails with a clear error.
Build a comprehensive error handler
Build a comprehensive error handler
Handle all common failure types in your server-side code. Map each error type to an appropriate user message and log the details for debugging.
1function handlePaymentError(error) {2 switch (error.type) {3 case 'card_error':4 return {5 status: 402,6 message: getDeclineMessage(error.decline_code || error.code),7 retry: true,8 };9 case 'validation_error':10 return {11 status: 400,12 message: 'Please check your payment details and try again.',13 retry: true,14 };15 case 'invalid_request_error':16 console.error('Stripe integration error:', error.message);17 return {18 status: 500,19 message: 'Something went wrong. Please try again.',20 retry: false,21 };22 default:23 console.error('Unexpected Stripe error:', error);24 return {25 status: 500,26 message: 'An unexpected error occurred.',27 retry: false,28 };29 }30}Expected result: Your error handler categorizes failures and provides appropriate user messages and retry guidance for each type.
Test with Stripe test cards
Test with Stripe test cards
Use Stripe test cards to simulate every failure scenario. This ensures your error handling works correctly before going live. Teams at RapidDev routinely test all decline paths during payment integration audits.
1// Test cards for payment failures2// 4242424242424242 — Always succeeds3// 4000000000000002 — Generic decline4// 4000000000009995 — Insufficient funds5// 4000000000000069 — Expired card6// 4000002500003155 — Requires 3D Secure authentication7// 4000000000000101 — Incorrect CVC (check fails)Expected result: Each test card triggers the expected failure path. Your error handler returns the correct user message for each scenario.
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 getDeclineMessage(code) {9 const messages = {10 insufficient_funds: 'Insufficient funds. Please use a different card.',11 expired_card: 'Your card has expired. Please update your payment method.',12 incorrect_cvc: 'Incorrect CVC. Please check and try again.',13 processing_error: 'Processing error. Please try again in a moment.',14 generic_decline: 'Card declined. Please try a different payment method.',15 fraudulent: 'Card declined. Please contact your bank.',16 lost_card: 'Card declined. Please contact your bank.',17 stolen_card: 'Card declined. Please contact your bank.',18 };19 return messages[code] || 'Payment failed. Please try a different payment method.';20}2122app.post('/api/pay', async (req, res) => {23 const { amount, currency, paymentMethodId } = req.body;2425 try {26 const paymentIntent = await stripe.paymentIntents.create({27 amount,28 currency: currency || 'usd',29 payment_method: paymentMethodId,30 confirm: true,31 automatic_payment_methods: {32 enabled: true,33 allow_redirects: 'never',34 },35 });3637 if (paymentIntent.status === 'requires_action') {38 return res.json({39 requiresAction: true,40 clientSecret: paymentIntent.client_secret,41 });42 }4344 res.json({ success: true, id: paymentIntent.id });45 } catch (err) {46 const error = err.raw || err;47 console.error('Payment failed:', {48 type: error.type,49 code: error.code,50 decline_code: error.decline_code,51 message: error.message,52 });5354 if (error.code === 'card_declined') {55 return res.status(402).json({56 error: getDeclineMessage(error.decline_code),57 decline_code: error.decline_code,58 retry: true,59 });60 }6162 res.status(500).json({63 error: 'Payment could not be processed. Please try again.',64 retry: error.type !== 'invalid_request_error',65 });66 }67});6869// Debug endpoint — retrieve PaymentIntent details70app.get('/api/payment/:id', async (req, res) => {71 const pi = await stripe.paymentIntents.retrieve(req.params.id);72 res.json({73 status: pi.status,74 amount: pi.amount,75 currency: pi.currency,76 error: pi.last_payment_error,77 });78});7980const PORT = process.env.PORT || 3000;81app.listen(PORT, () => console.log(`Server on port ${PORT}`));Common mistakes when fixing Stripe payment failed issue
Why it's a problem: Not checking the PaymentIntent status after creation
How to avoid: Always check paymentIntent.status. It may be 'requires_action' (3D Secure needed) or 'requires_payment_method' (failed) even after calling confirm.
Why it's a problem: Ignoring the last_payment_error field
How to avoid: Read paymentIntent.last_payment_error for the specific failure reason. This field contains the decline code, error type, and message.
Why it's a problem: Showing generic error messages for all payment failures
How to avoid: Map specific decline codes to actionable messages. 'Insufficient funds' is more helpful than 'Payment failed' because the customer knows to try a different card.
Why it's a problem: Not handling 3D Secure (requires_action) on the frontend
How to avoid: Use stripe.confirmPayment() with Stripe.js to handle 3D Secure authentication automatically. Without this, payments requiring authentication will always fail.
Best practices
- Always check paymentIntent.status after creation — don't assume success
- Read last_payment_error for specific decline codes and error types
- Map decline codes to user-friendly, actionable messages
- Handle 'requires_action' status by triggering 3D Secure on the frontend
- Log full error details server-side for debugging while showing simple messages to users
- Use the Stripe Dashboard Events tab to inspect failed payment details
- Test every failure path with Stripe test cards before going live
- Implement retry logic for transient errors like processing_error
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express payment endpoint that creates a Stripe PaymentIntent, handles card_declined errors with specific decline code messages, handles requires_action for 3D Secure, and includes a debug endpoint to retrieve PaymentIntent details. Map fraud-related declines to generic messages.
Build a comprehensive Stripe payment handler in Node.js that handles all failure types: card declines with specific codes, 3D Secure authentication requirements, validation errors, and API errors. Include a decline code message mapper and a debug endpoint.
Frequently asked questions
What does PaymentIntent status 'requires_action' mean?
It means the customer's bank requires additional authentication, usually 3D Secure. Use stripe.confirmPayment() on the frontend with Stripe.js to trigger the authentication flow. The payment isn't failed — it's waiting for the customer to authenticate.
How do I find out why a specific payment failed?
Check paymentIntent.last_payment_error for the error details. Also check Dashboard → Developers → Events and filter for payment_intent.payment_failed to see the full event timeline.
Should I automatically retry failed payments?
For one-time payments, don't auto-retry without customer action. For subscriptions, enable Smart Retries in Dashboard → Settings → Billing. Auto-retrying declines like insufficient_funds won't help — the customer needs to resolve the issue first.
What test card triggers 3D Secure authentication?
Use card number 4000002500003155 in test mode. It always requires 3D Secure authentication. Use 4242424242424242 for a card that always succeeds without authentication.
Why does my payment fail with 'invalid_request_error'?
This error type means your API request has a problem — missing required fields, invalid parameter values, or referencing a nonexistent object. Check the error message for the specific field that's wrong. This is a code bug, not a customer issue.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation