Error Handling
Complete guide to handling errors in the Babylon API.
Error Response Format
All errors follow a consistent format. Simple errors return:
{
"error": "Human-readable error message"
}Structured errors (BabylonError instances) return:
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"details": {
// Additional context (optional, development only)
}
}Validation errors return:
{
"error": "Validation failed",
"details": [
{
"field": "amount",
"message": "Expected number, received string"
}
]
}HTTP Status Codes
| Status | Meaning | When Used |
|---|---|---|
| 200 | OK | Successful request |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Invalid request data |
| 401 | Unauthorized | Authentication required/failed |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn’t exist |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
| 503 | Service Unavailable | Service temporarily down |
Error Codes
Authentication Errors
| Code | HTTP | Description |
|---|---|---|
AUTH_FAILED | 401 | Authentication required or failed |
AUTH_NO_TOKEN | 401 | Missing authentication token |
AUTH_INVALID_TOKEN | 401 | Token expired or invalid |
AUTH_EXPIRED_TOKEN | 401 | Token has expired |
AUTH_INVALID_CREDENTIALS | 401 | Invalid credentials |
FORBIDDEN | 403 | Insufficient permissions |
FORBIDDEN | 403 | Admin access required |
Example:
{
"error": "Authentication required"
}Or with code (development only):
{
"error": "Invalid or expired authentication token",
"code": "AUTH_FAILED"
}Validation Errors
| Code | HTTP | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Validation failed |
BAD_REQUEST | 400 | Invalid request data |
Example:
{
"error": "Validation failed",
"details": [
{
"field": "amount",
"message": "Expected number, received string"
},
{
"field": "outcome",
"message": "Invalid enum value. Expected 'YES' | 'NO'"
}
]
}Resource Errors
| Code | HTTP | Description |
|---|---|---|
NOT_FOUND | 404 | Resource doesn’t exist |
CONFLICT | 409 | Resource conflict (e.g., duplicate) |
Example:
{
"error": "Market not found: market-999",
"code": "NOT_FOUND"
}Or for conflicts:
{
"error": "Duplicate entry for field(s): username",
"fields": ["username"]
}Trading Errors
| Code | HTTP | Description |
|---|---|---|
INSUFFICIENT_FUNDS | 400 | Not enough funds |
TRADING_ERROR | 400 | Trading operation failed |
POSITION_ERROR | 400 | Position operation failed |
NOT_FOUND | 404 | Position doesn’t exist |
Example:
{
"error": "Insufficient funds. Required: 100, Available: 50",
"code": "INSUFFICIENT_FUNDS"
}Rate Limit Errors
| Code | HTTP | Description |
|---|---|---|
RATE_LIMIT | 429 | Too many requests |
Example:
{
"error": "Rate limit exceeded",
"code": "RATE_LIMIT"
}Headers:
Retry-After: 45Social Errors
| Code | HTTP | Description |
|---|---|---|
BAD_REQUEST | 400 | Invalid social operation |
CONFLICT | 409 | Already following/blocked/etc |
NOT_FOUND | 404 | Resource not found |
Server Errors
| Code | HTTP | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Internal server error |
DATABASE_ERROR | 500 | Database operation failed |
EXTERNAL_SERVICE_ERROR | 503 | External service unavailable |
SERVICE_UNAVAILABLE | 503 | Service temporarily down |
Error Handling Patterns
TypeScript/JavaScript
async function buyShares(marketId: string, side: 'yes' | 'no', amount: number) {
try {
const response = await fetch(`/api/markets/predictions/${marketId}/buy`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ side, amount })
});
const data = await response.json();
// Check for error response
if (data.error) {
// Handle error
const errorCode = data.code || 'UNKNOWN_ERROR';
switch (errorCode) {
case 'INSUFFICIENT_FUNDS':
alert(`Not enough funds: ${data.error}`);
break;
case 'NOT_FOUND':
alert('Market not found');
break;
case 'RATE_LIMIT':
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
alert(`Rate limited. Try again in ${retryAfter} seconds`);
setTimeout(() => buyShares(marketId, side, amount), retryAfter * 1000);
break;
default:
alert(`Error: ${data.error}`);
}
return null;
}
return data;
} catch (error) {
// Network error
console.error('Network error:', error);
alert('Network error. Please check your connection.');
return null;
}
}React Hook
import { useState } from 'react';
export function useAPICall<T>(
apiCall: () => Promise<T>
) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<T | null>(null);
const execute = async () => {
setLoading(true);
setError(null);
try {
const result = await apiCall();
setData(result);
return result;
} catch (err: any) {
const errorMessage = err.error?.message || err.message || 'Unknown error';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
return { execute, loading, error, data };
}
// Usage
function BuySharesButton() {
const { execute, loading, error } = useAPICall(
() => buyShares('market-123', 'YES', 100)
);
return (
<div>
<button onClick={execute} disabled={loading}>
{loading ? 'Buying...' : 'Buy Shares'}
</button>
{error && <div className="error">{error}</div>}
</div>
);
}Python
import requests
def buy_shares(market_id, outcome, amount, token):
url = f'https://babylon.market/api/markets/predictions/{market_id}/buy'
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
data = {
'outcome': outcome,
'amount': amount
}
try:
response = requests.post(url, headers=headers, json=data)
result = response.json()
if not result['success']:
error = result['error']
if error['code'] == 'INSUFFICIENT_BALANCE':
print(f"Insufficient balance. Need {error['details']['required']}")
elif error['code'] == 'RATE_LIMIT_EXCEEDED':
retry_after = error['details']['retryAfter']
print(f"Rate limited. Retry in {retry_after}s")
time.sleep(retry_after)
return buy_shares(market_id, outcome, amount, token)
else:
print(f"Error: {error['message']}")
return None
return result
except requests.RequestException as e:
print(f"Network error: {e}")
return NoneRetry Strategies
Exponential Backoff
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (data.error && data.code === 'RATE_LIMIT') {
// Wait based on retry-after header
const retryAfter = parseInt(response.headers.get('Retry-After') || String(Math.pow(2, i)));
await sleep(retryAfter * 1000);
continue;
}
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
lastError = error;
// Exponential backoff: 1s, 2s, 4s
await sleep(Math.pow(2, i) * 1000);
}
}
throw lastError;
}Circuit Breaker
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailure > 60000) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= 5) {
this.state = 'OPEN';
}
}
}Validation Errors
Zod validation errors include detailed field information:
{
"error": "Validation failed",
"details": [
{
"field": "amount",
"message": "Expected number, received string"
},
{
"field": "side",
"message": "Invalid enum value. Expected 'yes' | 'no'"
}
]
}Best Practices
1. Always Check for Error Field
const data = await response.json();
if (data.error) {
// Handle error
handleError(data);
return;
}
// Use data safely
console.log(data);2. Handle Specific Error Codes
function handleError(response: { error: string; code?: string }) {
const code = response.code || 'UNKNOWN_ERROR';
switch (code) {
case 'INSUFFICIENT_FUNDS':
showDepositModal();
break;
case 'RATE_LIMIT':
showRateLimitWarning();
break;
case 'AUTH_FAILED':
case 'AUTH_NO_TOKEN':
case 'AUTH_INVALID_TOKEN':
redirectToLogin();
break;
default:
showGenericError(response.error);
}
}3. Implement Retries for Transient Errors
const RETRYABLE_CODES = [
'INTERNAL_ERROR',
'DATABASE_ERROR',
'EXTERNAL_SERVICE_ERROR',
'SERVICE_UNAVAILABLE'
];
if (RETRYABLE_CODES.includes(errorCode)) {
// Retry with exponential backoff
await retryWithBackoff();
}4. Log Errors for Debugging
if (data.error) {
console.error('API Error:', {
code: data.code || 'UNKNOWN',
message: data.error,
endpoint: url,
method: 'POST',
timestamp: new Date().toISOString()
});
}5. Show User-Friendly Messages
const USER_FRIENDLY_MESSAGES: Record<string, string> = {
INSUFFICIENT_FUNDS: 'You don\'t have enough funds for this trade.',
NOT_FOUND: 'This resource no longer exists.',
RATE_LIMIT: 'You\'re making requests too quickly. Please slow down.',
VALIDATION_ERROR: 'Please check your input and try again.',
AUTH_FAILED: 'Please log in to continue.'
};
function getUserMessage(errorCode: string | undefined, defaultMessage: string): string {
if (errorCode && USER_FRIENDLY_MESSAGES[errorCode]) {
return USER_FRIENDLY_MESSAGES[errorCode];
}
return defaultMessage || 'An error occurred. Please try again.';
}Error Recovery
Automatic Recovery
class RobustAPIClient {
async request(endpoint: string, options: RequestInit) {
let attempt = 0;
while (attempt < 3) {
try {
const response = await fetch(endpoint, options);
const data = await response.json();
if (data.error) {
const errorCode = data.code || 'UNKNOWN_ERROR';
// Handle specific errors
if (errorCode === 'RATE_LIMIT') {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
await sleep(retryAfter * 1000);
attempt++;
continue;
}
if (this.isRetryable(errorCode)) {
await sleep(Math.pow(2, attempt) * 1000);
attempt++;
continue;
}
throw new Error(data.error);
}
return data;
} catch (error) {
if (attempt === 2) throw error;
attempt++;
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
private isRetryable(code: string): boolean {
return ['INTERNAL_ERROR', 'DATABASE_ERROR', 'EXTERNAL_SERVICE_ERROR', 'SERVICE_UNAVAILABLE'].includes(code);
}
}Debugging
Enable Verbose Logging
const DEBUG = process.env.NODE_ENV === 'development';
async function apiCall(url: string, options: RequestInit) {
if (DEBUG) {
console.log('API Request:', { url, method: options.method });
}
const response = await fetch(url, options);
const data = await response.json();
if (DEBUG) {
console.log('API Response:', {
hasError: !!data.error,
status: response.status,
data: data.error ? { error: data.error, code: data.code } : data
});
}
return data;
}Error Tracking
import * as Sentry from '@sentry/nextjs';
async function apiCall(url: string, options: RequestInit) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (data.error) {
// Track API errors
Sentry.captureMessage('API Error', {
level: 'error',
extra: {
code: data.code || 'UNKNOWN',
message: data.error,
endpoint: url,
details: data.details
}
});
}
return data;
} catch (error) {
Sentry.captureException(error);
throw error;
}
}Common Error Scenarios
Scenario 1: Insufficient Balance
// User tries to buy shares but doesn't have enough funds
const response = await buyShares('market-123', 'yes', 100);
if (response && response.error && response.code === 'INSUFFICIENT_FUNDS') {
// Extract shortfall from error message if available
const errorMsg = response.error;
// Show deposit modal
showModal({
title: 'Insufficient Balance',
message: errorMsg || 'You don\'t have enough funds for this trade.',
actions: [
{ label: 'Deposit', onClick: () => openDepositPage() },
{ label: 'Cancel', onClick: () => closeModal() }
]
});
}Scenario 2: Rate Limiting
// User hits rate limit
const response = await fetch('/api/markets/predictions/123/buy', options);
const data = await response.json();
if (data.error && data.code === 'RATE_LIMIT') {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
// Show countdown and retry
showNotification({
message: `Please wait ${retryAfter} seconds before trying again`,
type: 'warning'
});
// Auto-retry after delay
setTimeout(() => {
buyShares(marketId, side, amount);
}, retryAfter * 1000);
}Scenario 3: Authentication Expired
// Token expired mid-session
const response = await fetch('/api/users/me', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.error && (data.code === 'AUTH_INVALID_TOKEN' || data.code === 'AUTH_FAILED')) {
// Token expired, refresh
const newToken = await refreshAuthToken();
// Retry with new token
return fetch('/api/users/me', {
headers: { 'Authorization': `Bearer ${newToken}` }
});
}Testing Error Handling
Unit Tests
describe('Error Handling', () => {
it('handles insufficient balance', async () => {
const mockFetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve({
error: 'Insufficient funds. Required: 100, Available: 50',
code: 'INSUFFICIENT_FUNDS'
})
});
global.fetch = mockFetch;
const result = await buyShares('market-123', 'yes', 100);
expect(result).toBeNull();
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});Error Monitoring
Sentry Integration
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig({
// Your Next.js config
}, {
silent: true,
org: "your-org",
project: "babylon"
});// sentry.client.config.js
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
beforeSend(event, hint) {
// Filter out expected errors
if (event.exception?.values?.[0]?.value?.includes('RATE_LIMIT')) {
return null; // Don't send to Sentry
}
return event;
}
});Next Steps
Last updated on