Error handling
Anticipate and handle API errors, network timeouts, and payment failures with robust retry strategies.
A reliable payment integration does more than handle the happy path — it anticipates failures. Here are proven patterns for handling all error categories from the Wajub API.
Mechanics vs. practice
This page brings together resilience patterns (retry, backoff, circuit breaker). For code reference, see HTTP codes & error structure and the Failure reasons catalog.
Error classification
| Category | HTTP code | Meaning | Recommended action |
|---|---|---|---|
| Success | 2xx | Operation successful | Handle normally |
| Client error | 400 | Invalid data (missing parameter, unknown currency) | Fix the call, do not retry |
| Authentication | 401 | Missing or invalid API key | Check the key, update |
| Authorization | 403 | Insufficient permission (e.g. sk_… required) | Use the correct key |
| Not found | 404 | Nonexistent resource | Check the reference |
| Conflict | 409 | Idempotency or duplicate | Check existing state |
| Rate limit | 429 | Too many requests | Retry with exponential backoff |
| Server error | 5xx | Internal Wajub error | Retry with backoff, alert if persists |
Never retry a 400
A 400 error means the request is malformed. Retrying will yield the same
result. You must correct the parameters before trying again.
Retry strategy
429 and 5xx errors are retryable. Use exponential backoff with jitter to
avoid overloading the API:
async function withRetry(fn, { maxRetries = 3, baseDelay = 1000 } = {}) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
const isRetryable =
err.status === 429 ||
err.status >= 500 ||
err.code === 'ECONNRESET' ||
err.code === 'ETIMEDOUT';
if (!isRetryable || attempt === maxRetries) {
throw err;
}
const delay = baseDelay * Math.pow(2, attempt - 1) * (0.5 + Math.random());
console.warn(`Attempt ${attempt} failed, retrying in ${Math.round(delay)}ms`, {
status: err.status,
message: err.message,
});
await new Promise(r => setTimeout(r, delay));
}
}
}Handling by operation type
Payments — initialization
Payment initialization is a sensitive operation. If the call fails after the request was received by the server (timeout, disconnection), you risk creating a duplicate.
async function initializePayment(order) {
const reference = `ORDER-${order.id}-T${Date.now()}`;
try {
const { transaction, authorization_url } = await withRetry(() =>
wajub.payments.initialize({
amount: order.amount,
currency: order.currency,
customer: { email: order.email },
reference,
})
);
await order.save({ trx: transaction.reference });
return authorization_url;
} catch (err) {
if (err.status === 400) {
// Bad request: fix, do not retry
throw new Error(`Invalid parameters: ${err.message}`);
}
// Network or server error: there may be a duplicate
// → search for the transaction by reference before deciding
throw err;
}
}Webhooks — processing errors
A failing handler must not prevent the 200 acknowledgment:
const eventQueue = [];
app.post('/webhooks/wajub', (req, res) => {
let event;
try {
event = wajub.webhooks.constructEvent(req.body, req.headers['x-wajub-signature'], SECRET);
} catch {
return res.status(400).send('Invalid signature');
}
// Immediate ACK
res.sendStatus(200);
// Async processing with error handling
processEvent(event).catch(err => {
console.error('Webhook processing failed', {
eventId: event.id,
type: event.type,
error: err.message,
});
});
});
async function processEvent(event) {
if (event.type === 'payment.succeeded') {
await withRetry(() => fulfillOrder(event.data.reference));
}
}Circuit breaker pattern
For prolonged outages, implement a circuit breaker that suspends API calls rather than accumulating failures:
class CircuitBreaker {
constructor({ failureThreshold = 5, resetTimeout = 30000 }) {
this.failures = 0;
this.state = 'CLOSED';
this.resetTimeout = resetTimeout;
this.failureThreshold = failureThreshold;
}
async call(fn) {
if (this.state === 'OPEN') {
throw new Error('Circuit breaker is OPEN');
}
try {
const result = await fn();
this.failures = 0;
return result;
} catch (err) {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
setTimeout(() => {
this.state = 'HALF_OPEN';
this.failures = 0;
}, this.resetTimeout);
}
throw err;
}
}
}Circuit breaker in production
In production, use proven libraries like opossum (Node.js) or resilience4j (Java) rather than a homegrown implementation.