Error Handling
This guide provides comprehensive information about error handling in the ESFA Backend API, including error response formats, status codes, common scenarios, and best practices for robust integration.
All ESFA Backend API endpoints use a consistent error response format called the ErrorEnvelope. This standardized structure makes it easier to handle errors programmatically.
Standard Error Structure
{
"errors": [
{
"i18N": "MATCH_NOT_FOUND",
"error": "Match not found",
"type": "NotFound"
}
]
}
Fields Description
Field | Type | Description |
|---|
errors
| array | Array of error objects (may contain multiple errors) |
errors[].i18N
| string | Internationalization key for programmatic handling |
errors[].error
| string | Human-readable error message |
errors[].type
| string | Error type classification |
HTTP Status Codes
The API uses standard HTTP status codes to indicate the type of error:
Client Errors (4xx)
400 Bad Request
Invalid request data or malformed payload.
Common causes:
Example:
{
"errors": [
{
"i18N": "INVALID_RESULTS",
"error": "Invalid match results data",
"type": "BadRequest"
}
]
}
Handling:
Validate request data before sending
Check API documentation for required fields
Ensure proper JSON formatting
401 Unauthorized
Authentication required or token invalid.
Common causes:
Example:
{
"errors": [
{
"i18N": "UNAUTHORIZED",
"error": "Invalid or missing authentication token",
"type": "Unauthorized"
}
]
}
Handling:
Obtain fresh token from Authentication Service
Verify Authorization header format: Bearer <token>
Implement automatic token refresh
403 Forbidden
Valid authentication but insufficient permissions.
Common causes:
Example:
{
"errors": [
{
"i18N": "FORBIDDEN",
"error": "Access denied",
"type": "Forbidden"
}
]
}
Handling:
Check account permissions
Contact administrator for access
Verify correct API credentials
404 Not Found
Requested resource does not exist.
Common causes:
Invalid match ID
Invalid game code
Deleted resources
Incorrect endpoint URL
Example:
{
"errors": [
{
"i18N": "MATCH_NOT_FOUND",
"error": "Match not found",
"type": "NotFound"
}
]
}
Handling:
Verify resource identifiers
Check if resource was deleted
Confirm correct endpoint URL
409 Conflict
Resource state conflict.
Common causes:
Example:
{
"errors": [
{
"i18N": "MATCH_ALREADY_FINISHED",
"error": "Match has already been finished",
"type": "Conflict"
}
]
}
Handling:
410 Gone
Resource expired or permanently deleted.
Common causes:
Match exceeded TTL
Resource archived
System cleanup
Example:
{
"errors": [
{
"i18N": "MATCH_EXPIRED",
"error": "Match has expired",
"type": "Gone"
}
]
}
Handling:
Server Errors (5xx)
500 Internal Server Error
Temporary server-side issue.
Common causes:
Database connectivity issues
Service dependencies down
Internal processing errors
Resource exhaustion
Example:
{
"errors": [
{
"i18N": "SERVER_ERROR",
"error": "Internal server error",
"type": "ServerError"
}
]
}
Handling:
Common Error Scenarios
Authentication Errors
Token Expired
// Detect expired token
if (response.status === 401) {
const errorData = await response.json();
if (errorData.errors[0]?.i18N === 'UNAUTHORIZED') {
// Token likely expired, refresh it
await refreshToken();
// Retry original request
return retryRequest(originalRequest);
}
}
Invalid Credentials
// Handle authentication service errors
async function authenticateWithRetry(credentials, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const token = await getToken(credentials);
return token;
} catch (error) {
if (error.response?.status === 401) {
throw new Error('Invalid credentials - check API key and secret');
}
if (attempt === maxRetries) {
throw error;
}
// Exponential backoff for server errors
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
Match Operation Errors
Match Not Found
async function getMatchSafely(gameCode, matchId, token) {
try {
const match = await getMatch(gameCode, matchId, token);
return { success: true, data: match };
} catch (error) {
if (error.response?.status === 404) {
return {
success: false,
error: 'MATCH_NOT_FOUND',
message: 'The requested match could not be found'
};
}
throw error; // Re-throw unexpected errors
}
}
Match Already Finished
async function finishMatchSafely(gameCode, matchId, results, token) {
try {
await finishMatch(gameCode, matchId, results, token);
return { success: true };
} catch (error) {
if (error.response?.status === 409) {
const errorData = await error.response.json();
if (errorData.errors[0]?.i18N === 'MATCH_ALREADY_FINISHED') {
return {
success: false,
error: 'ALREADY_FINISHED',
message: 'Match has already been completed'
};
}
}
throw error;
}
}
Validation Errors
Invalid Match Results
function validateAndFormatResults(results) {
const errors = [];
if (!Array.isArray(results) || results.length === 0) {
errors.push('Results must be a non-empty array');
}
const places = new Set();
const playerIds = new Set();
results.forEach((result, index) => {
// Check required fields
if (!result.playerId) {
errors.push(`Result ${index}: playerId is required`);
}
if (typeof result.place !== 'number' || result.place < 1) {
errors.push(`Result ${index}: place must be positive number`);
}
if (typeof result.score !== 'number') {
errors.push(`Result ${index}: score must be numeric`);
}
// Check for duplicates
if (places.has(result.place)) {
errors.push(`Duplicate place ${result.place} found`);
}
if (playerIds.has(result.playerId)) {
errors.push(`Duplicate playerId ${result.playerId} found`);
}
places.add(result.place);
playerIds.add(result.playerId);
});
// Check consecutive places
const sortedPlaces = Array.from(places).sort((a, b) => a - b);
for (let i = 0; i < sortedPlaces.length; i++) {
if (sortedPlaces[i] !== i + 1) {
errors.push(`Missing place ${i + 1}. Places must be consecutive`);
break;
}
}
if (errors.length > 0) {
throw new ValidationError(errors);
}
return results;
}
Best Practices
Error Handling Strategy
1. Categorize Errors
class APIErrorHandler {
static categorizeError(error) {
const status = error.response?.status;
if (status >= 400 && status < 500) {
return {
category: 'CLIENT_ERROR',
retryable: status === 401, // Only retry auth errors
userMessage: this.getUserMessage(error)
};
}
if (status >= 500) {
return {
category: 'SERVER_ERROR',
retryable: true,
userMessage: 'Service temporarily unavailable. Please try again.'
};
}
return {
category: 'UNKNOWN',
retryable: false,
userMessage: 'An unexpected error occurred.'
};
}
}
2. Implement Retry Logic
async function retryableRequest(requestFn, options = {}) {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 10000,
retryCondition = (error) => error.response?.status >= 500
} = options;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
// Don't retry if not retryable
if (!retryCondition(error) || attempt === maxRetries) {
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(
baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000,
maxDelay
);
console.log(`Request failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
await sleep(delay);
}
}
}
3. Circuit Breaker Pattern
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.monitoringPeriod = options.monitoringPeriod || 10000;
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.resetTimeout) {
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';
}
}
}
User Experience
1. User-Friendly Error Messages
const ERROR_MESSAGES = {
MATCH_NOT_FOUND: 'The game match you\'re looking for doesn\'t exist or has been removed.',
MATCH_EXPIRED: 'This match has expired. Please start a new game.',
MATCH_ALREADY_FINISHED: 'This match has already been completed.',
UNAUTHORIZED: 'Your session has expired. Please refresh the page and try again.',
SERVER_ERROR: 'We\'re experiencing technical difficulties. Please try again in a few moments.',
NETWORK_ERROR: 'Unable to connect to the server. Please check your internet connection.'
};
function getUserFriendlyMessage(error) {
const i18nKey = error.response?.data?.errors?.[0]?.i18N;
return ERROR_MESSAGES[i18nKey] || ERROR_MESSAGES.SERVER_ERROR;
}
2. Progress Indication
class APIClient {
async finishMatchWithProgress(gameCode, matchId, results, onProgress) {
try {
onProgress({ status: 'validating', message: 'Validating results...' });
validateResults(results);
onProgress({ status: 'submitting', message: 'Submitting results...' });
const response = await this.finishMatch(gameCode, matchId, results);
onProgress({ status: 'complete', message: 'Match completed successfully!' });
return response;
} catch (error) {
const message = getUserFriendlyMessage(error);
onProgress({ status: 'error', message });
throw error;
}
}
}
Logging and Monitoring
1. Structured Error Logging
function logError(error, context = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
level: 'ERROR',
message: error.message,
stack: error.stack,
context,
api: {
method: context.method,
url: context.url,
status: error.response?.status,
response: error.response?.data
},
user: {
id: context.userId,
session: context.sessionId
}
};
// Remove sensitive data
if (logEntry.context.headers) {
delete logEntry.context.headers.Authorization;
}
console.error(JSON.stringify(logEntry));
// Send to monitoring service
if (typeof sendToMonitoring === 'function') {
sendToMonitoring(logEntry);
}
}
2. Error Metrics
class ErrorMetrics {
static increment(errorType, endpoint) {
// Increment error counter
if (typeof metrics !== 'undefined') {
metrics.increment('api.errors', {
type: errorType,
endpoint: endpoint
});
}
}
static recordLatency(duration, success) {
if (typeof metrics !== 'undefined') {
metrics.histogram('api.request_duration', duration, {
success: success.toString()
});
}
}
}
Error Reference
Complete Error Code Reference
i18N Key | HTTP Status | Description | Retry? | User Action |
|---|
UNAUTHORIZED
| 401 | Invalid/missing token | Yes* | Refresh session |
FORBIDDEN
| 403 | Access denied | No | Check permissions |
MATCH_NOT_FOUND
| 404 | Match doesn't exist | No | Verify match ID |
MATCH_EXPIRED
| 410 | Match has expired | No | Start new match |
MATCH_ALREADY_FINISHED
| 409 | Match completed | No | View results |
INVALID_RESULTS
| 400 | Invalid match data | No | Fix data format |
INVALID_REQUEST
| 400 | Malformed request | No | Check request format |
INVALID_CREDENTIALS
| 401 | Wrong API keys | No | Update credentials |
SERVER_ERROR
| 500 | Internal error | Yes | Wait and retry |
*Only after obtaining fresh token
Integration Checklist
✅ Error Response Parsing
Parse ErrorEnvelope format correctly
Handle multiple errors in response
Extract i18N keys for programmatic handling
✅ HTTP Status Code Handling
Implement specific logic for each status code
Distinguish between client and server errors
Handle edge cases (network timeouts, etc.)
✅ Retry Strategy
✅ User Experience
Show user-friendly error messages
Provide actionable error guidance
Implement loading states and progress indicators
✅ Monitoring and Logging
Log errors with sufficient context
Track error rates and patterns
Set up alerts for critical errors
05 September 2025