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.