//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Gestiona los errores de Stripe con elegancia categorizando los tipos de error, mostrando mensajes amigables, administrando flujos de autenticación 3D Secure e implementando lógica de reintento para fallos transitorios.
Define una utilidad de manejo de errores:
// lib/stripe-errors.ts
import type { StripeError } from "@stripe/stripe-js";
import type Stripe from "stripe";
export function getClientErrorMessage(error: StripeError): string {
switch (error.type) {
case "card_error":
return getCardErrorMessage(error.code);
case "validation_error":
return "Revisa los datos de pago e inténtalo de nuevo.";
case "invalid_request_error":
return "Algo salió mal. Inténtalo de nuevo.";
case "api_error":
return "Nuestro procesador de pagos no está disponible temporalmente. Inténtalo de nuevo en un momento.";
case "api_connection_error":
return "Error de red. Comprueba tu conexión e inténtalo de nuevo.";
case "authentication_error":
return "La autenticación falló. Inténtalo de nuevo.";
case "rate_limit_error":
return "Demasiadas solicitudes. Espera un momento e inténtalo de nuevo.";
default:
return "Ocurrió un error inesperado. Inténtalo de nuevo.";
}
}
function getCardErrorMessage(code: string | undefined): string {
switch (code) {
case "card_declined":
return "Tu tarjeta fue rechazada. Prueba con otra tarjeta.";
case "insufficient_funds":
return "Fondos insuficientes. Prueba con otra tarjeta.";
case "expired_card":
return "Tu tarjeta ha caducado. Usa otra tarjeta.";
case "incorrect_cvc":
return "CVC incorrecto. Compruébalo e inténtalo de nuevo.";
case "incorrect_number":
return "Número de tarjeta incorrecto. Compruébalo e inténtalo de nuevo.";
case "processing_error":
return "Ocurrió un error al procesar tu tarjeta. Inténtalo de nuevo.";
default:
return "Tu tarjeta fue rechazada. Prueba con otro método de pago.";
}
}
export function getServerErrorMessage(error: Stripe.errors.StripeError): string {
switch (error.type) {
case "StripeCardError":
return error.message ?? "Error de tarjeta.";
case "StripeInvalidRequestError":
return "Solicitud inválida. Contacta con soporte.";
case "StripeAPIError":
return "Servicio de pago temporalmente no disponible.";
case "StripeConnectionError":
return "No se pudo conectar con el servicio de pago.";
case "StripeAuthenticationError":
return "Error de configuración de pago. Contacta con soporte.";
case "StripeRateLimitError":
return "Demasiadas solicitudes. Inténtalo de nuevo más tarde.";
default:
return "Ocurrió un error inesperado.";
}
}Maneja la autenticación 3D Secure / SCA:
// lib/confirm-payment.ts
import type { Stripe, StripeElements } from "@stripe/stripe-js";
interface PaymentResult {
success: boolean;
error?: string;
requiresAction?: boolean;
}
export async function confirmPaymentWithSCA(
stripe: Stripe,
elements: StripeElements,
returnUrl: string
): Promise<PaymentResult> {
const { error: submitError } = await elements.submit();
if (submitError) {
return { success: false, error: submitError.message };
}
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: returnUrl },
redirect: "if_required",
});
if (error) {
return { success: false, error: error.message };
}
switch (paymentIntent?.status) {
case "succeeded":
return { success: true };
case "processing":
return {
success: false,
error: "Tu pago se está procesando. Te avisaremos cuando se complete.",
};
case "requires_action":
// Se activó 3D Secure pero no se completó
return {
success: false,
requiresAction: true,
error: "Se requiere autenticación adicional.",
};
default:
return { success: false, error: "Estado de pago inesperado." };
}
}// app/checkout/secure-checkout-form.tsx
"use client";
import { useState, type FormEvent } from "react";
import {
PaymentElement,
useStripe,
useElements,
} from "@stripe/react-stripe-js";
import { getClientErrorMessage } from "@/lib/stripe-errors";
import type { StripeError } from "@stripe/stripe-js";
type FormStatus = "idle" | "validating" | "processing" | "succeeded" | "failed";
export function SecureCheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [status, setStatus] = useState<FormStatus>("idle");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
function handleError(error: StripeError) {
const message = getClientErrorMessage(error);
setErrorMessage(message);
setStatus("failed");
// Permite reintento para errores transitorios
if (error.type === "api_error" || error.type === "api_connection_error") {
setRetryCount((prev) => prev + 1);
}
}
async function handleSubmit(e: FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
setStatus("validating");
setErrorMessage(null);
// Paso 1: Validar el formulario
const { error: submitError } = await elements.submit();
if (submitError) {
handleError(submitError);
return;
}
setStatus("processing");
// Paso 2: Confirmar el pago
const { error: confirmError, paymentIntent } =
await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
redirect: "if_required",
});
if (confirmError) {
handleError(confirmError);
return;
}
// Paso 3: Manejar el resultado
switch (paymentIntent?.status) {
case "succeeded":
setStatus("succeeded");
break;
case "processing":
setErrorMessage(
"Tu pago se está procesando. Te avisaremos cuando se complete."
);
setStatus("processing");
break;
case "requires_action":
setErrorMessage(
"Se requiere autenticación adicional. Completa la verificación."
);
setStatus("failed");
break;
default:
setErrorMessage("Algo salió mal. Inténtalo de nuevo.");
setStatus("failed");
}
}
if (status === "succeeded") {
return (
<div className="text-center p-8">
<div className="text-green-500 text-5xl mb-4">✓</div>
<h2 className="text-2xl font-bold">¡Pago exitoso!</h2>
<p className="text-gray-600 mt-2">
Gracias por tu compra.
</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 space-y-6">
<PaymentElement />
{errorMessage && (
<div
className={`rounded-lg p-4 ${
status === "processing"
? "bg-yellow-50 border border-yellow-200"
: "bg-red-50 border border-red-200"
}`}
role="alert"
>
<p
className={`text-sm font-medium ${
status === "processing" ? "text-yellow-800" : "text-red-800"
}`}
>
{errorMessage}
</p>
</div>
)}
<button
type="submit"
disabled={
!stripe || status === "validating" || status === "processing"
}
className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium
hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{status === "validating" && "Validando..."}
{status === "processing" && "Procesando pago..."}
{status === "idle" && "Pagar ahora"}
{status === "failed" && (retryCount > 0 ? `Reintentar (${retryCount})` : "Intentar de nuevo")}
</button>
{retryCount >= 3 && (
<p className="text-sm text-gray-500 text-center">
¿Tienes problemas?{" "}
<a href="/support" className="text-blue-600 underline">
Contacta con soporte
</a>
</p>
)}
</form>
);
}Manejo de errores en el servidor:
// app/actions/safe-checkout.ts
"use server";
import { stripe } from "@/lib/stripe";
import { getServerErrorMessage } from "@/lib/stripe-errors";
import Stripe from "stripe";
export async function safeCreateCheckout(priceId: string) {
try {
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});
return { url: session.url };
} catch (err) {
if (err instanceof Stripe.errors.StripeError) {
console.error(`Stripe error [${err.type}]: ${err.message}`);
return { error: getServerErrorMessage(err) };
}
console.error("Unexpected error:", err);
return { error: "Ocurrió un error inesperado." };
}
}StripeError de @stripe/stripe-js) y los del lado del servidor (Stripe.errors.StripeError de stripe) tienen jerarquías de tipos diferentes pero categorías similares.card_error es el tipo más común. Significa que el banco emisor rechazó la tarjeta. El campo code proporciona el motivo específico (fondos insuficientes, caducada, etc.).validation_error ocurre cuando la entrada del usuario no pasa la validación del lado del cliente de Stripe.js (formato de número de tarjeta inválido, campos faltantes).requires_action indica que el flujo está en curso.api_error y api_connection_error son transitorios. Son seguros de reintentar porque las operaciones de Stripe son idempotentes cuando pasas un idempotencyKey.confirmPayment con redirect: "if_required" solo redirige para métodos de pago que lo requieran (como 3D Secure). Para pagos simples con tarjeta, se resuelve en el mismo lugar.Reintento con backoff exponencial (lado del servidor):
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (
err instanceof Stripe.errors.StripeError &&
(err.type === "StripeConnectionError" ||
err.type === "StripeAPIError") &&
attempt < maxRetries
) {
const delay = Math.pow(2, attempt) * 1000;
await new Promise((r) => setTimeout(r, delay));
continue;
}
throw err;
}
}
throw new Error("Max retries exceeded");
}
// Uso
const session = await withRetry(() =>
stripe.checkout.sessions.create({ /* ... */ })
);Solicitudes idempotentes:
const paymentIntent = await stripe.paymentIntents.create(
{
amount: 2999,
currency: "usd",
automatic_payment_methods: { enabled: true },
},
{
idempotencyKey: `pi_${userId}_${orderId}`,
}
);StripeError del lado del cliente se importa desde @stripe/stripe-js.Stripe.errors.StripeError del paquete stripe.error.code en errores de tarjeta está tipada como string | undefined. Usa una sentencia switch con literales de string para el estrechamiento de tipos.import type { StripeError } from "@stripe/stripe-js";
function isRetryable(error: StripeError): boolean {
return (
error.type === "api_error" || error.type === "api_connection_error"
);
}confirmPayment puede redirigir al usuario para 3D Secure. Cuando regrese, comprueba el estado del PaymentIntent mediante el parámetro de consulta payment_intent.requires_action no significa que el pago falló. Significa que el usuario debe completar un paso adicional (como 3D Secure). No muestres un mensaje de fallo para este estado.api_connection_error puede deberse a la red del usuario, no a los servidores de Stripe. Reintentar puede no ayudar si el usuario está sin conexión.Stripe.errors.StripeError de forma específica. Otros errores (como timeouts de red) requieren un manejo diferente.card_error ni invalid_request_error. Son fallos permanentes que requieren acción del usuario (otra tarjeta o entrada corregida).rate_limit_error) deben manejarse con backoff. El límite de tasa de Stripe es de 100 solicitudes por segundo en modo live.| Tipo de error | Causa | Reintentable | Acción del usuario |
|---|---|---|---|
| card_error | Tarjeta rechazada por el emisor | No | Usar otra tarjeta |
| validation_error | Entrada de formulario inválida | No | Corregir campos de entrada |
| invalid_request_error | Parámetros de API incorrectos | No | Corregir código o entrada |
| api_error | Problema en el servidor de Stripe | Sí | Reintentar tras una espera |
| api_connection_error | Fallo de red | Sí | Comprobar conexión, reintentar |
| authentication_error | Clave de API inválida | No | Corregir configuración |
| rate_limit_error | Demasiadas solicitudes | Sí | Reintentar con backoff |
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Mapeo de errores personalizado | Control total sobre los mensajes | Hay que mantener el mapa de errores |
| Mensajes predeterminados de Stripe | Menos código, siempre actualizados | Demasiado técnicos para usuarios finales |
| Error boundary + fallback | Captura errores inesperados de React | No captura errores de Stripe.js |
| Notificaciones toast | Visualización de errores no bloqueante | Fácil de pasar por alto en errores de pago |
StripeError de @stripe/stripe-js (usado en código del navegador).Stripe.errors.StripeError del paquete Node stripe.api_error y api_connection_error son transitorios y seguros de reintentar.rate_limit_error es reintentable con backoff.card_error, validation_error e invalid_request_error son fallos permanentes que requieren acción del usuario.redirect: "if_required" solo redirige para métodos de pago que lo necesiten.requires_action significa que el usuario debe completar un paso adicional; no significa que el pago falló.getClientErrorMessage.async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (isRetryableStripeError(err) && attempt < maxRetries) {
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
continue;
}
throw err;
}
}
throw new Error("Max retries exceeded");
}pi_${userId}_${orderId} en el objeto de opciones.requires_action significa que el usuario debe completar la autenticación 3D Secure.return_url con un parámetro de consulta payment_intent.type FormStatus = "idle" | "validating" | "processing" | "succeeded" | "failed";retryCount solo se incrementa para tipos de error transitorios.// Lado del cliente
import type { StripeError } from "@stripe/stripe-js";
// Lado del servidor
import type Stripe from "stripe";
// Stripe.errors.StripeErrorerror.code en errores de tarjeta es string | undefined.api_connection_error puede deberse a la red del usuario, no solo a los servidores de Stripe.instanceof Stripe.errors.StripeError para identificar errores específicos de Stripe.getServerErrorMessage.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥