Error handling
HTTP error codes, SDK error types, rate limiting, idempotency, and debugging patterns
This guide covers how to handle errors from the npayload API and SDK, implement safe retries, and debug delivery failures.
HTTP error codes
The npayload API uses standard HTTP status codes.
| Code | Meaning | Retryable |
|---|---|---|
200 | Success | N/A |
201 | Created | N/A |
400 | Bad request (invalid parameters) | No |
401 | Unauthorized (missing or invalid token) | No (re-authenticate first) |
403 | Forbidden (insufficient scopes) | No |
404 | Resource not found | No |
409 | Conflict (duplicate idempotency key with different payload) | No |
422 | Validation error (schema mismatch) | No |
429 | Rate limited | Yes (after backoff) |
500 | Internal server error | Yes |
502 | Bad gateway | Yes |
503 | Service unavailable | Yes |
All error responses follow a consistent format:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Retry after 2 seconds.",
"retryAfter": 2
}
}SDK error types
The SDK throws typed errors that you can catch and handle specifically.
import {
NPayloadClient,
NPayloadError,
NPayloadTimeoutError,
NPayloadRateLimitError,
NPayloadValidationError,
} from '@npayload/node';
try {
await npayload.messages.publish('orders', {
payload: { event: 'order.created' },
});
} catch (error) {
if (error instanceof NPayloadRateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
} else if (error instanceof NPayloadValidationError) {
console.log('Validation failed:', error.details);
} else if (error instanceof NPayloadTimeoutError) {
console.log('Request timed out');
} else if (error instanceof NPayloadError) {
console.log(`API error ${error.statusCode}: ${error.message}`);
}
}| Error class | When thrown |
|---|---|
NPayloadError | Base class for all API errors |
NPayloadTimeoutError | Request exceeded the configured timeout |
NPayloadRateLimitError | API returned 429. Includes retryAfter in seconds |
NPayloadValidationError | API returned 400 or 422. Includes details with field-level errors |
NPayloadAuthError | API returned 401 or 403. Token is invalid, expired, or lacks required scopes |
Rate limiting
npayload enforces rate limits per organisation to protect the platform and ensure fair usage.
Rate limit headers
Every response includes rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
Handling rate limits
The SDK provides built-in retry with backoff for rate limit errors.
const npayload = new NPayloadClient({
auth,
retries: {
maxRetries: 3,
retryOnRateLimit: true, // Automatically wait and retry on 429
},
});For manual handling:
try {
await npayload.messages.publish('orders', { payload: data });
} catch (error) {
if (error instanceof NPayloadRateLimitError) {
await sleep(error.retryAfter * 1000);
await npayload.messages.publish('orders', { payload: data });
}
}Idempotency for safe retries
Network errors and timeouts can leave you unsure whether a request succeeded. Use idempotency keys to make retries safe.
const idempotencyKey = `order-${orderId}-${Date.now()}`;
async function publishWithRetry(payload: any, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await npayload.messages.publish('orders', {
payload,
idempotencyKey,
});
} catch (error) {
if (error instanceof NPayloadTimeoutError && attempt < maxRetries) {
continue; // Safe to retry with the same idempotency key
}
if (error instanceof NPayloadRateLimitError) {
await sleep(error.retryAfter * 1000);
continue;
}
throw error; // Non-retryable error
}
}
}Idempotency keys are valid for 24 hours. If you retry with the same key and the original request succeeded, npayload returns the original response without creating a duplicate message.
Circuit breaker patterns
When calling npayload from a critical path, implement a client-side circuit breaker to fail fast during outages.
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private readonly threshold = 5;
private readonly cooldown = 30000; // 30 seconds
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.isOpen()) {
throw new Error('Circuit breaker is open');
}
try {
const result = await fn();
this.failures = 0;
return result;
} catch (error) {
this.failures++;
this.lastFailure = Date.now();
throw error;
}
}
private isOpen(): boolean {
if (this.failures < this.threshold) return false;
return Date.now() - this.lastFailure < this.cooldown;
}
}
const breaker = new CircuitBreaker();
await breaker.execute(() =>
npayload.messages.publish('orders', { payload: data })
);Debugging delivery failures
When messages are not reaching your consumers, check these areas in order.
1. Check the subscription status
const sub = await npayload.subscriptions.get('sub_abc123');
console.log(sub.status); // "active" | "paused"
console.log(sub.circuitState); // "closed" | "open" | "half-open"If the circuit is open, your endpoint has been failing. Fix the endpoint and the circuit will reset.
2. Check the dead letter queue
const stats = await npayload.dlq.getStats({
subscriptionGid: 'sub_abc123',
});
if (stats.totalEntries > 0) {
const entries = await npayload.dlq.list({
subscriptionGid: 'sub_abc123',
limit: 5,
});
for (const entry of entries.items) {
console.log(entry.failureReason);
}
}3. Verify routing key filters
If your subscription uses a routing key filter, make sure published messages match the pattern.
// Subscription filter: "order.created.*"
// This message matches:
await npayload.messages.publish('events', {
payload: data,
routingKey: 'order.created.us-east',
});
// This message does NOT match:
await npayload.messages.publish('events', {
payload: data,
routingKey: 'order.updated.us-east',
});4. Check message delivery status
const delivery = await npayload.messages.getDeliveryStatus('msg_abc123');
for (const attempt of delivery.attempts) {
console.log(attempt.subscriptionGid);
console.log(attempt.status); // "delivered" | "failed" | "pending"
console.log(attempt.statusCode); // HTTP status from your endpoint
console.log(attempt.attemptedAt);
}Configuring timeouts
Set request timeouts to prevent your application from hanging on slow responses.
const npayload = new NPayloadClient({
auth,
timeout: 10000, // 10 seconds
});For individual requests:
await npayload.messages.publish('orders', {
payload: data,
}, { timeout: 5000 });Best practices
- Always catch
NPayloadRateLimitErrorand respect theretryAftervalue. Aggressive retrying only extends the rate limit window - Use idempotency keys for all publish operations in critical paths
- Implement client-side circuit breakers to fail fast when npayload is experiencing issues
- Log the full error object (including
requestIdfromNPayloadError) for debugging. Include the request ID when contacting support - Set reasonable timeouts. The default is 30 seconds, but most operations complete in under 1 second
- Handle
NPayloadAuthErrorby refreshing your token (or let the SDK handle it automatically) - Monitor DLQ depth and delivery status as leading indicators of integration health
Next steps
Was this page helpful?