Error Handling Guide
Use this to guide you in error handling for your application to gracefully handle API failures, rate limits, and network issues.
HTTP Status Codes
| Code | Error | Meaning | Action |
|---|---|---|---|
| 401 | Unauthorized | Invalid API key | Verify key, check revocation |
| 402 | Payment Required | CU pool exhausted | Enable overage or upgrade tier |
| 403 | Forbidden | Insufficient permissions | Check API key scopes |
| 429 | Too Many Requests | Rate limit exceeded (CUPs) | Implement backoff |
| 500 | Internal Server Error | Server-side issue | Retry with backoff |
| 503 | Service Unavailable | Maintenance or overload | Check status page, retry |
Rate Limit Errors (429)
Understanding 429 Errors
You'll receive a 429 when you exceed your CUPs (Compute Units Per Second) limit:
{
"error": "Rate limit exceeded",
"message": "Throughput limit exceeded. Current: 15500 CUPs, Limit: 15000 CUPs",
"retry_after": 2,
"cups_limit": 15000,
"current_cups": 15500,
"tier": "Business"
}
Response Headers:
X-RateLimit-CUPs-Limit: 10000
X-RateLimit-CUPs-Remaining: 0
Retry-After: 2
Exponential Backoff (Python)
import time
import random
from typing import Callable, TypeVar, Any
T = TypeVar('T')
def exponential_backoff(
func: Callable[[], T],
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0
) -> T:
"""
Retry function with exponential backoff.
Args:
func: Function to retry
max_retries: Maximum retry attempts
base_delay: Initial delay in seconds
max_delay: Maximum delay cap
Returns:
Function result
Raises:
Last exception if all retries fail
"""
for attempt in range(max_retries):
try:
return func()
except RateLimitError as e:
if attempt == max_retries - 1:
raise
# Exponential backoff: 1s, 2s, 4s, 8s, 16s
delay = min(base_delay * (2 ** attempt), max_delay)
# Add jitter to prevent thundering herd
jitter = random.uniform(0, 0.1 * delay)
total_delay = delay + jitter
print(f"Rate limited. Retry {attempt + 1}/{max_retries} after {total_delay:.2f}s")
time.sleep(total_delay)
raise Exception("Max retries exceeded")
# Usage
from axol import AxolClient
client = AxolClient(api_key="your_key")
def get_block():
return client.get_block("ethereum", "latest")
block = exponential_backoff(get_block)
Exponential Backoff (JavaScript/TypeScript)
interface RetryOptions {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
}
async function exponentialBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxRetries = 5,
baseDelay = 1000, // milliseconds
maxDelay = 60000
} = options;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (error.status !== 429 || attempt === maxRetries - 1) {
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * 0.1 * delay;
const totalDelay = delay + jitter;
console.log(`Rate limited. Retry ${attempt + 1}/${maxRetries} after ${totalDelay}ms`);
await new Promise(resolve => setTimeout(resolve, totalDelay));
}
}
throw new Error('Max retries exceeded');
}
// Usage
import { AxolClient } from '@axol/api-client';
const client = new AxolClient({ apiKey: 'your_key' });
const block = await exponentialBackoff(
() => client.getBlock('ethereum', 'latest')
);
Rate Limiting Strategy
Proactive approach - Track your CUPs usage:
class RateLimiter:
def __init__(self, cups_limit: int):
self.cups_limit = cups_limit
self.window_start = time.time()
self.cups_consumed = 0
def can_request(self, cu_cost: int) -> bool:
"""Check if request fits within rate limit"""
now = time.time()
# Reset window every second
if now - self.window_start >= 1.0:
self.window_start = now
self.cups_consumed = 0
return self.cups_consumed + cu_cost <= self.cups_limit
def consume(self, cu_cost: int):
"""Record CU consumption"""
self.cups_consumed += cu_cost
def wait_if_needed(self, cu_cost: int):
"""Sleep if request would exceed limit"""
if not self.can_request(cu_cost):
wait_time = 1.0 - (time.time() - self.window_start)
if wait_time > 0:
time.sleep(wait_time)
self.window_start = time.time()
self.cups_consumed = 0
# Usage
limiter = RateLimiter(cups_limit=15000) # Business tier
for _ in range(1000):
limiter.wait_if_needed(cu_cost=26) # eth_call cost
result = client.eth_call(...)
limiter.consume(26)
CU Pool Exhausted (402)
Understanding 402 Errors
Triggered when you've consumed your monthly CU allocation:
{
"error": "CU pool exhausted",
"message": "Monthly standard CU pool exhausted. 600M/600M CUs used.",
"cu_limit": 900000000,
"cu_used": 900000000,
"cu_remaining": 0,
"reset_date": "2025-11-10T00:00:00Z",
"tier": "Professional",
"options": {
"enable_overage": true,
"overage_rate": 0.50,
"upgrade_tier": "Business"
}
}
Decision Tree
def handle_cu_exhausted(error_response):
"""
Handle 402 CU pool exhausted error
Options:
1. Enable overage billing (costs extra)
2. Upgrade to higher tier (more CUs)
3. Wait until reset (free)
4. Optimize usage (reduce CU consumption)
"""
cu_used = error_response['cu_used']
reset_date = error_response['reset_date']
overage_rate = error_response['options']['overage_rate']
# Option 1: How much would overage cost?
additional_cus_needed = estimate_remaining_month_usage()
overage_cost = (additional_cus_needed / 1_000_000) * overage_rate
print(f"Overage billing: ${overage_cost:.2f} for {additional_cus_needed:,} CUs")
# Option 2: Next tier pricing
next_tier = error_response['options']['upgrade_tier']
print(f"Upgrade to {next_tier}: Check /docs/pricing")
# Option 3: Time until reset
import datetime
reset = datetime.fromisoformat(reset_date)
time_left = reset - datetime.now()
print(f"Reset in: {time_left.days} days, {time_left.seconds // 3600} hours")
# Option 4: Optimize usage
print("Optimization tips:")
print("- Cache responses for static data")
print("- Avoid trace methods when unnecessary")
print("- Use narrow block ranges for eth_getLogs")
print("- Batch requests when possible")
Graceful Degradation
class AxolClientWithFallback:
def __init__(self, primary_key, fallback_key=None):
self.primary = AxolClient(api_key=primary_key)
self.fallback = AxolClient(api_key=fallback_key) if fallback_key else None
self.using_fallback = False
def get_block(self, chain, block_number):
try:
if not self.using_fallback:
return self.primary.get_block(chain, block_number)
except CUPoolExhaustedError:
if self.fallback:
print("Primary CU pool exhausted, switching to fallback")
self.using_fallback = True
return self.fallback.get_block(chain, block_number)
raise
return self.fallback.get_block(chain, block_number)
Authentication Errors (401, 403)
401 Unauthorized
try:
client = AxolClient(api_key="your_key")
result = client.get_block("ethereum", "latest")
except AuthenticationError as e:
# Check common issues
print("Authentication failed:")
print("1. Verify API key is correct (check for spaces)")
print("2. Check if key was revoked at app.axol.io")
print("3. Ensure environment variable is set correctly")
# Log for debugging
import os
key = os.getenv('AXOL_API_KEY', '')
print(f"Key starts with: {key[:8]}...")
print(f"Key length: {len(key)} characters")
403 Forbidden
try:
result = client.submit_transaction(...)
except InsufficientPermissionsError as e:
print(f"API key lacks required scope: {e.required_scope}")
print("Solutions:")
print("1. Create new key with 'write:transactions' scope")
print("2. Use different key for write operations")
print("3. Contact support if scope should be available")
Server Errors (500, 503)
Retry Strategy
def is_retryable(status_code: int) -> bool:
"""Determine if error is worth retrying"""
return status_code in [429, 500, 502, 503, 504]
def should_retry(error, attempt: int, max_retries: int) -> bool:
"""Enhanced retry logic"""
if attempt >= max_retries:
return False
# Always retry rate limits and server errors
if hasattr(error, 'status_code'):
return is_retryable(error.status_code)
# Retry network errors
if isinstance(error, (ConnectionError, TimeoutError)):
return True
return False
# Usage
for attempt in range(5):
try:
result = client.get_block("ethereum", "latest")
break
except Exception as e:
if should_retry(e, attempt, max_retries=5):
wait = min(2 ** attempt, 32)
print(f"Retry {attempt + 1}/5 after {wait}s: {e}")
time.sleep(wait)
else:
raise
Circuit Breaker Pattern
Prevent cascading failures when API is down:
from enum import Enum
import time
class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if recovered
class CircuitBreaker:
def __init__(
self,
failure_threshold: int = 5,
timeout: float = 60.0,
recovery_timeout: float = 30.0
):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = CircuitState.CLOSED
def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
# Check if we should try to recover
if time.time() - self.last_failure_time >= self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
# Success - reset if in half-open state
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
# Trip breaker if threshold exceeded
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
print(f"Circuit breaker OPEN after {self.failure_count} failures")
raise
# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30.0)
for _ in range(10):
try:
result = breaker.call(client.get_block, "ethereum", "latest")
print(f"Success: {result}")
except Exception as e:
print(f"Failed: {e}")
time.sleep(1)
Best Practices
1. Always Check Response Headers
response = client.get_block("ethereum", "latest")
# Monitor CU usage
cu_remaining = response.headers.get('X-RateLimit-CU-Remaining')
cu_limit = response.headers.get('X-RateLimit-CU-Limit')
usage_percent = (1 - int(cu_remaining) / int(cu_limit)) * 100
if usage_percent > 80:
print(f"WARNING: {usage_percent:.1f}% of monthly CU pool used")
# Monitor throughput
cups_remaining = response.headers.get('X-RateLimit-CUPs-Remaining')
if int(cups_remaining) < 1000:
print("WARNING: Approaching CUPs limit, slow down requests")
2. Implement Timeouts
# Set appropriate timeouts
client = AxolClient(
api_key="your_key",
timeout=30 # Default timeout
)
# Use higher timeout for trace methods
response = client.debug_trace_transaction(
tx_hash="0x...",
timeout=60 # Trace methods can take 30+ seconds
)
3. Cache Aggressively
from functools import lru_cache
from datetime import datetime, timedelta
class CachingClient:
def __init__(self, api_key):
self.client = AxolClient(api_key=api_key)
self._cache = {}
def get_block(self, chain, block_number):
# Only cache historical blocks (not 'latest')
if block_number == 'latest':
return self.client.get_block(chain, block_number)
cache_key = f"{chain}:{block_number}"
if cache_key in self._cache:
return self._cache[cache_key]
# Fetch and cache
block = self.client.get_block(chain, block_number)
self._cache[cache_key] = block
return block
4. Monitor and Alert
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('axol_client')
class MonitoredClient:
def __init__(self, api_key):
self.client = AxolClient(api_key=api_key)
self.error_count = 0
self.total_requests = 0
def get_block(self, chain, block_number):
self.total_requests += 1
try:
result = self.client.get_block(chain, block_number)
# Log CU usage
cu_used = result.headers.get('X-Request-CU-Cost', 0)
logger.info(f"Block request: {cu_used} CUs")
return result
except Exception as e:
self.error_count += 1
error_rate = self.error_count / self.total_requests
logger.error(f"Error: {e}")
# Alert if error rate too high
if error_rate > 0.1: # 10% error rate
logger.critical(f"High error rate: {error_rate:.1%}")
raise
Error Response Examples
Complete Error Structure
{
"error": "rate_limit_exceeded",
"message": "Throughput limit exceeded",
"status": 429,
"request_id": "req_abc123",
"timestamp": "2025-11-10T15:30:00Z",
"details": {
"cups_limit": 10000,
"cups_used": 10500,
"retry_after": 2
},
"documentation": "https://docs.axol.io/docs/concepts/rate-limits"
}
Handling in Code
try:
result = client.get_block("ethereum", "latest")
except AxolAPIError as e:
print(f"Error: {e.error}")
print(f"Message: {e.message}")
print(f"Request ID: {e.request_id}") # Include in support tickets
print(f"Docs: {e.documentation}")
if hasattr(e, 'retry_after'):
print(f"Retry after {e.retry_after} seconds")
Summary
Key Takeaways:
- Implement exponential backoff for 429 errors
- Monitor CU usage proactively (80%/90%/100% alerts)
- Cache historical data - blocks never change
- Use circuit breakers for cascading failure prevention
- Log request IDs for support tickets
- Set appropriate timeouts - trace methods need 60s+
- Handle 402 gracefully - enable overage or wait for reset
See also:
- Rate Limits - Understanding limits
- Compute Units - CU cost optimization
- Troubleshooting - Common issues