Skip to main content

Handle Payout Failures and Retries

Learn how to effectively handle payout failures and implement retry mechanisms to ensure reliable payment processing with NextAPI.

Overview

Payment processing can fail for various reasons - insufficient funds, network issues, invalid recipient details, or temporary service disruptions. This guide shows you how to build robust error handling and retry logic.

Common Failure Scenarios

1. Insufficient Balance

{
"error": "INSUFFICIENT_BALANCE",
"message": "Wallet does not have sufficient funds for this transaction"
}

2. Invalid Recipient

{
"error": "INVALID_RECIPIENT",
"message": "Recipient account details are invalid or not found"
}

3. Temporary Service Issues

{
"error": "SERVICE_UNAVAILABLE",
"message": "Payment service is temporarily unavailable"
}

Error Handling Strategy

1. Categorize Errors

function categorizeError(error) {
const retryableErrors = [
'SERVICE_UNAVAILABLE',
'NETWORK_TIMEOUT',
'RATE_LIMIT_EXCEEDED'
];

const userActionErrors = [
'INSUFFICIENT_BALANCE',
'INVALID_RECIPIENT',
'ACCOUNT_FROZEN'
];

if (retryableErrors.includes(error.code)) {
return 'RETRYABLE';
} else if (userActionErrors.includes(error.code)) {
return 'USER_ACTION';
} else {
return 'PERMANENT';
}
}

2. Implement Retry Logic

class PayoutRetryHandler {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}

async executeWithRetry(payoutFunction, ...args) {
let lastError;

for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const result = await payoutFunction(...args);

// Log successful retry
if (attempt > 1) {
console.log(`Payout succeeded on attempt ${attempt}`);
}

return result;
} catch (error) {
lastError = error;
const errorCategory = this.categorizeError(error);

if (errorCategory === 'PERMANENT') {
throw error; // Don't retry permanent errors
}

if (errorCategory === 'USER_ACTION') {
// Queue for manual review
await this.queueForManualReview(error, args);
throw error;
}

if (attempt === this.maxRetries) {
// Max retries reached, escalate
await this.escalateFailure(error, args);
throw error;
}

// Calculate delay with exponential backoff
const delay = this.calculateDelay(attempt);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
await this.sleep(delay);
}
}
}

categorizeError(error) {
const retryableErrors = ['SERVICE_UNAVAILABLE', 'NETWORK_TIMEOUT', 'RATE_LIMIT_EXCEEDED'];
const userActionErrors = ['INSUFFICIENT_BALANCE', 'INVALID_RECIPIENT', 'ACCOUNT_FROZEN'];

if (retryableErrors.includes(error.code)) return 'RETRYABLE';
if (userActionErrors.includes(error.code)) return 'USER_ACTION';
return 'PERMANENT';
}

calculateDelay(attempt) {
// Exponential backoff with jitter
const exponentialDelay = this.baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 0.1 * exponentialDelay;
return exponentialDelay + jitter;
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

async queueForManualReview(error, args) {
// Store failed payout for manual review
await fetch('https://api.nextpay.world/v2/payouts/queue-review', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
error: error,
payoutData: args,
queuedAt: new Date().toISOString()
})
});
}

async escalateFailure(error, args) {
// Send alert to monitoring system
await fetch('https://api.nextpay.world/v2/alerts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'PAYOUT_FAILURE',
severity: 'HIGH',
message: `Payout failed after ${this.maxRetries} attempts`,
error: error,
payoutData: args
})
});
}
}

Implementation Example

1. Basic Payout with Retry

const retryHandler = new PayoutRetryHandler(3, 1000);

async function processPayout(walletId, amount, recipient) {
return await retryHandler.executeWithRetry(async () => {
const response = await fetch('https://api.nextpay.world/v2/payouts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
walletId: walletId,
amount: amount,
recipient: recipient
})
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}

return await response.json();
});
}

2. Batch Payout Processing

async function processBatchPayouts(payouts) {
const results = [];

for (const payout of payouts) {
try {
const result = await processPayout(
payout.walletId,
payout.amount,
payout.recipient
);

results.push({
id: payout.id,
status: 'SUCCESS',
result: result
});
} catch (error) {
results.push({
id: payout.id,
status: 'FAILED',
error: error.message
});
}
}

return results;
}

Monitoring and Alerting

1. Track Failure Rates

class PayoutMonitor {
constructor() {
this.failureCount = 0;
this.successCount = 0;
}

recordSuccess() {
this.successCount++;
this.checkFailureRate();
}

recordFailure() {
this.failureCount++;
this.checkFailureRate();
}

checkFailureRate() {
const total = this.successCount + this.failureCount;
if (total >= 100) { // Check every 100 transactions
const failureRate = this.failureCount / total;

if (failureRate > 0.05) { // 5% failure rate threshold
this.sendAlert('High failure rate detected', {
failureRate: failureRate,
totalTransactions: total,
failures: this.failureCount
});
}

// Reset counters
this.failureCount = 0;
this.successCount = 0;
}
}

async sendAlert(message, data) {
console.error(`ALERT: ${message}`, data);
// Send to your monitoring system
}
}

Best Practices

1. Idempotency

Always use idempotency keys to prevent duplicate processing:

async function processPayoutWithIdempotency(walletId, amount, recipient) {
const idempotencyKey = `payout_${walletId}_${Date.now()}`;

const response = await fetch('https://api.nextpay.world/v2/payouts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
walletId: walletId,
amount: amount,
recipient: recipient
})
});

return await response.json();
}

2. Circuit Breaker Pattern

class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureThreshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}

async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}

try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}

onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();

if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}

Testing Your Error Handling

1. Simulate Failures

// Test with sandbox environment
async function testFailureHandling() {
try {
// Test insufficient balance
await processPayout('test-wallet', 999999, 'test-recipient');
} catch (error) {
console.log('Insufficient balance error handled correctly');
}

try {
// Test invalid recipient
await processPayout('test-wallet', 100, 'invalid-recipient');
} catch (error) {
console.log('Invalid recipient error handled correctly');
}
}

Conclusion

Implementing robust error handling and retry mechanisms ensures:

  • ✅ Reliable payment processing
  • ✅ Automatic recovery from temporary failures
  • ✅ Proper escalation for critical issues
  • ✅ Comprehensive monitoring and alerting
  • ✅ Better user experience with fewer failed transactions

Ready to implement? Start with our basic payout guide and add these error handling patterns.