sickn33/agentic-awesome-skills · critical · Error
Payment failed
Error message
Payment failed
What it means
Illustrative domain error from the payment step of processOrder: chargePayment returned { success: false } and the guard throws 'Payment failed'. In the BEFORE shape this leaves the worst ambiguity — inventory may be reserved and the next try block must guess whether to refund or log.
Source
Thrown at skills/fp-async/SKILL.md:152
```typescript
// BEFORE: Deeply nested, hard to follow
async function processOrder(orderId: string) {
try {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
try {
const user = await fetchUser(order.userId)
if (!user) throw new Error('User not found')
try {
const inventory = await checkInventory(order.items)
if (!inventory.available) throw new Error('Out of stock')
try {
const payment = await chargePayment(user, order.total)
if (!payment.success) throw new Error('Payment failed')
try {
const shipment = await createShipment(order, user)
return { order, shipment, payment }
} catch (e) {
// Refund payment? Log? What's the state now?
await refundPayment(payment.id)
throw e
}
} catch (e) {
throw e
}
} catch (e) {
throw e
}
} catch (e) {
throw e
}View on GitHub (pinned to 58d857988f)
Solutions
- Log the payment processor's decline code — the generic 'Payment failed' message hides the real reason
- Make chargePayment idempotent with an idempotency key so retries are safe
- Sequence compensation explicitly (refund on shipment failure) instead of nested catch blocks — or use the skill's TaskEither pipeline with explicit rollback in error paths
- Validate the payment method (expiry, billing info) before charging
Example fix
// before
const payment = await chargePayment(user, order.total)
if (!payment.success) throw new Error('Payment failed')
// after
pipe(
chargePaymentTask(user, order.total),
TE.chain(payment => payment.success
? TE.right(payment)
: TE.left(Object.assign(
new Error('Payment failed'),
{ code: payment.declineCode }
)))
) Defensive patterns
Strategy: retry
Validate before calling
// pre-charge validation
if (!user.paymentMethod || user.paymentMethod.expiry < new Date())
return { kind: 'InvalidPaymentMethod' } Type guard
const isPaymentResult = (r: unknown): r is { success: boolean; declineCode?: string } =>
typeof r === 'object' && r !== null && typeof (r as any).success === 'boolean' Try / catch
try {
const payment = await chargePayment(user, order.total)
if (!payment.success) throw Object.assign(new Error('Payment failed'), { code: payment.declineCode })
} catch (e) {
// compensating action: release reserved inventory, then rethrow or queue retry
await releaseInventory(order.items)
throw e
} Prevention
- Send an idempotency key on every charge request
- Never retry blindly — only on gateway timeouts with idempotency in place
- Implement explicit compensation (refund/release) for each step that already mutated state
When it happens
Trigger: Card declined by the processor; insufficient funds; gateway timeout after an uncertain capture; amount/currency mismatch causing processor rejection.
Common situations: Sandbox/test card numbers used against production gateways; expired stored payment methods; gateway outages; naive retries double-charging when the first attempt actually succeeded.
Related errors
AI-assisted analysis of sickn33/agentic-awesome-skills@58d857988f (2026-08-26).
Data as JSON: /api/errors/067d203a414605f8.
Report an issue: GitHub.