//
Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Lide com erros do Stripe de forma graciosa, categorizando tipos de erro, exibindo mensagens amigáveis ao usuário, gerenciando fluxos de autenticação 3D Secure e implementando lógica de retentativa para falhas transitórias.
Defina um utilitário de tratamento de erros:
// 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 "Por favor, verifique seus detalhes de pagamento e tente novamente.";
case "invalid_request_error":
return "Algo deu errado. Por favor, tente novamente.";
case "api_error":
return "Nosso processador de pagamento está temporariamente indisponível. Por favor, tente novamente em um momento.";
case "api_connection_error":
return "Erro de rede. Por favor, verifique sua conexão e tente novamente.";
case "authentication_error":
return "Falha na autenticação. Por favor, tente novamente.";
case "rate_limit_error":
return "Muitas solicitações. Por favor, aguarde um momento e tente novamente.";
default:
return "Ocorreu um erro inesperado. Por favor, tente novamente.";
}
}
function getCardErrorMessage(code: string | undefined): string {
switch (code) {
case "card_declined":
return "Seu cartão foi recusado. Por favor, use um cartão diferente.";
case "insufficient_funds":
return "Fundos insuficientes. Por favor, use um cartão diferente.";
case "expired_card":
return "Seu cartão expirou. Por favor, use um cartão diferente.";
case "incorrect_cvc":
return "CVC incorreto. Por favor, verifique e tente novamente.";
case "incorrect_number":
return "Número de cartão incorreto. Por favor, verifique e tente novamente.";
case "processing_error":
return "Ocorreu um erro ao processar seu cartão. Por favor, tente novamente.";
default:
return "Seu cartão foi recusado. Por favor, use um método de pagamento diferente.";
}
}
export function getServerErrorMessage(error: Stripe.errors.StripeError): string {
switch (error.type) {
case "StripeCardError":
return error.message ?? "Erro no cartão.";
case "StripeInvalidRequestError":
return "Solicitação inválida. Por favor, entre em contato com o suporte.";
case "StripeAPIError":
return "Serviço de pagamento temporariamente indisponível.";
case "StripeConnectionError":
return "Não foi possível conectar ao serviço de pagamento.";
case "StripeAuthenticationError":
return "Erro de configuração de pagamento. Por favor, entre em contato com o suporte.";
case "StripeRateLimitError":
return "Muitas solicitações. Por favor, tente novamente mais tarde.";
default:
return "Ocorreu um erro inesperado.";
}
}Lide com a autenticação 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: "Seu pagamento está sendo processado. Você será notificado quando ele for concluído.",
};
case "requires_action":
// 3D Secure foi acionado, mas não concluído
return {
success: false,
requiresAction: true,
error: "Autenticação adicional é necessária.",
};
default:
return { success: false, error: "Status de pagamento 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 retentativa para erros transitórios
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);
// Passo 1: Validar o formulário
const { error: submitError } = await elements.submit();
if (submitError) {
handleError(submitError);
return;
}
setStatus("processing");
// Passo 2: Confirmar o pagamento
const { error: confirmError, paymentIntent } =
await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
redirect: "if_required",
});
if (confirmError) {
handleError(confirmError);
return;
}
// Passo 3: Lidar com o resultado
switch (paymentIntent?.status) {
case "succeeded":
setStatus("succeeded");
break;
case "processing":
setErrorMessage(
"Seu pagamento está sendo processado. Notificaremos você quando ele for concluído."
);
setStatus("processing");
break;
case "requires_action":
setErrorMessage(
"Autenticação adicional necessária. Por favor, complete a verificação."
);
setStatus("failed");
break;
default:
setErrorMessage("Algo deu errado. Por favor, tente novamente.");
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">Pagamento bem-sucedido!</h2>
<p className="text-gray-600 mt-2">
Obrigado pela sua 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" && "Processando pagamento..."}
{status === "idle" && "Pagar agora"}
{status === "failed" && (retryCount > 0 ? `Tentar novamente (${retryCount})` : "Tentar novamente")}
</button>
{retryCount >= 3 && (
<p className="text-sm text-gray-500 text-center">
Com problemas?{" "}
<a href="/support" className="text-blue-600 underline">
Entre em contato com o suporte
</a>
</p>
)}
</form>
);
}Tratamento de erros no lado do 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(`Erro Stripe [${err.type}]: ${err.message}`);
return { error: getServerErrorMessage(err) };
}
console.error("Erro inesperado:", err);
return { error: "Ocorreu um erro inesperado." };
}
}StripeError de @stripe/stripe-js) e erros do lado do servidor (Stripe.errors.StripeError de stripe) têm hierarquias de tipo diferentes, mas categorias semelhantes.card_error é o tipo mais comum. Significa que o cartão foi recusado pelo banco emissor. O campo code fornece o motivo específico (fundos insuficientes, expirado, etc.).validation_error ocorre quando a entrada do usuário falha na validação do lado do cliente do Stripe.js (formato de número de cartão inválido, campos ausentes).requires_action indica que o fluxo está em andamento.api_error e api_connection_error são transitórios. Estes podem ser retentados com segurança porque as operações do Stripe são idempotentes quando você passa um idempotencyKey.confirmPayment com redirect: "if_required" apenas redireciona para métodos de pagamento que o exigem (como 3D Secure). Para pagamentos simples com cartão, ele é resolvido no local.Retentativa com backoff exponencial (lado do 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({ /* ... */ })
);Solicitações idempotentes:
const paymentIntent = await stripe.paymentIntents.create(
{
amount: 2999,
currency: "usd",
automatic_payment_methods: { enabled: true },
},
{
idempotencyKey: `pi_${userId}_${orderId}`,
}
);StripeError do lado do cliente é importado de @stripe/stripe-js.Stripe.errors.StripeError do pacote stripe.error.code em erros de cartão é tipada como string | undefined. Use uma instrução switch com literais de string para estreitamento de tipo.import type { StripeError } from "@stripe/stripe-js";
function isRetryable(error: StripeError): boolean {
return (
error.type === "api_error" || error.type === "api_connection_error"
);
}confirmPayment pode redirecionar o usuário para 3D Secure. Ao retornar, verifique o status do PaymentIntent através do parâmetro de consulta payment_intent.requires_action não significa que o pagamento falhou. Significa que o usuário precisa concluir uma etapa adicional (como 3D Secure). Não mostre uma mensagem de falha para este status.api_connection_error pode ser causado pela rede do usuário, não pelos servidores do Stripe. Retentar pode não ajudar se o usuário estiver offline.Stripe.errors.StripeError. Outros erros (como timeouts de rede) precisam de tratamento diferente.card_error ou invalid_request_error. Estas são falhas permanentes que exigem ação do usuário (cartão diferente ou entrada corrigida).rate_limit_error) devem ser tratados com backoff. O limite de taxa do Stripe é de 100 solicitações por segundo em modo live.| Tipo de Erro | Causa | Retentável | Ação do Usuário |
|---|---|---|---|
| card_error | Cartão recusado pelo emissor | Não | Use um cartão diferente |
| validation_error | Entrada de formulário inválida | Não | Corrija os campos de entrada |
| invalid_request_error | Parâmetros de API incorretos | Não | Corrija o código ou a entrada |
| api_error | Problema no servidor Stripe | Sim | Tente novamente após um atraso |
| api_connection_error | Falha de rede | Sim | Verifique a conexão, tente novamente |
| authentication_error | Chave de API inválida | Não | Corrija a configuração |
| rate_limit_error | Muitas solicitações | Sim | Tente novamente com backoff |
| Abordagem | Prós | Contras |
|---|---|---|
| Mapeamento de erros personalizado | Controle total sobre as mensagens | Precisa manter o mapa de erros |
| Mensagens padrão do Stripe | Menos código, sempre atualizado | Muito técnico para usuários finais |
| Error boundary + fallback | Captura erros inesperados do React | Não captura erros do Stripe.js |
| Notificações toast | Exibição de erros não bloqueante | Fácil de perder para erros de pagamento |
StripeError de @stripe/stripe-js (usado no código do navegador).Stripe.errors.StripeError do pacote Node stripe.api_error e api_connection_error são transitórios e seguros para retentar.rate_limit_error pode ser retentado com backoff.card_error, validation_error e invalid_request_error são falhas permanentes que exigem ação do usuário.redirect: "if_required" apenas redireciona para métodos de pagamento que precisam dele.requires_action significa que o usuário precisa concluir uma etapa adicional -- não significa que o pagamento falhou.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} no objeto de opções.requires_action significa que o usuário precisa concluir a autenticação 3D Secure.return_url com um parâmetro de consulta payment_intent.type FormStatus = "idle" | "validating" | "processing" | "succeeded" | "failed";retryCount é incrementado apenas para tipos de erro transitórios.// Lado do cliente
import type { StripeError } from "@stripe/stripe-js";
// Lado do servidor
import type Stripe from "stripe";
// Stripe.errors.StripeErrorerror.code em erros de cartão é string | undefined.api_connection_error pode ser causado pela rede do usuário, não apenas pelos servidores do Stripe.instanceof Stripe.errors.StripeError para identificar erros específicos do Stripe.getServerErrorMessage.Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥