Error Handling
The Batchmates API uses conventional HTTP response codes to indicate success or failure. Understanding these codes and error structures will help you build robust applications.
HTTP Status Codes
- Name
200 OK- Description
Request succeeded. The response body contains the requested data.
- Name
201 Created- Description
Resource created successfully. Common for POST requests that create new records.
- Name
400 Bad Request- Description
Invalid request format or business rule violation. Check request parameters.
- Name
401 Unauthorized- Description
Missing, invalid, or expired authentication token. User must log in again.
- Name
403 Forbidden- Description
Authenticated but insufficient permissions for this operation. Check user role.
- Name
404 Not Found- Description
Requested resource does not exist or user doesn't have access to it.
- Name
422 Unprocessable Entity- Description
Validation errors in request data. Check the
errorsobject for details.
- Name
429 Too Many Requests- Description
Rate limit exceeded. Wait before retrying (see
Retry-Afterheader).
- Name
500 Internal Server Error- Description
Server error occurred. These are logged and monitored - contact support if persistent.
- Name
503 Service Unavailable- Description
Service temporarily unavailable due to maintenance or overload. Retry later.
Error Response Format
All errors return JSON with a consistent structure:
Standard Error Structure
{
"success": false,
"message": "Human-readable error description",
"errors": {
"field_name": ["Specific validation error message"]
}
}
- Name
success- Type
- boolean
- Description
Always
falsefor error responses
- Name
message- Type
- string
- Description
High-level error description for display to users
- Name
errors- Type
- object
- Description
Optional field-specific validation errors (422 responses only)
Common Error Scenarios
Authentication Errors (401)
Missing Token
{
"success": false,
"message": "Unauthenticated."
}
Cause: Request missing Authorization header
Solution:
fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
})
Invalid or Expired Token
{
"success": false,
"message": "Unauthenticated."
}
Cause: Token is malformed, expired, or invalidated
Solution:
if (response.status === 401) {
// Clear stored token
localStorage.removeItem('auth_token');
// Redirect to login
window.location.href = '/login';
}
Permission Errors (403)
Insufficient Permissions
{
"success": false,
"message": "Unauthorized"
}
Cause: User authenticated but lacks required role or permissions
Examples:
- Donor trying to access admin endpoints
- User accessing another user's private data
- Non-admin trying to create campaigns
Solution:
if (response.status === 403) {
showError('You do not have permission to perform this action');
// Redirect to appropriate page
}
Campaign Creator Restrictions
{
"success": false,
"message": "Only campaign creator can edit pending campaigns"
}
Cause: Trying to edit someone else's pending campaign
Validation Errors (422)
Field Validation Errors
{
"success": false,
"message": "The given data was invalid.",
"errors": {
"email": [
"The email field is required."
],
"amount": [
"The amount must be at least 1.",
"The amount must be a number."
],
"payment_gateway": [
"The selected payment gateway is invalid."
]
}
}
Cause: Request data failed validation rules
Solution:
if (response.status === 422) {
const { errors } = await response.json();
// Display errors next to form fields
Object.keys(errors).forEach(field => {
const errorMessages = errors[field].join(' ');
showFieldError(field, errorMessages);
});
}
Business Rule Violations
{
"success": false,
"message": "Campaign is not accepting donations"
}
Cause: Campaign status is not active
Solution: Check campaign status before allowing donations
{
"success": false,
"message": "Cannot delete payment method with active subscriptions"
}
Cause: Payment method is linked to active recurring donations
Solution: Cancel subscriptions first or update them to use different payment method
Resource Not Found (404)
Entity Not Found
{
"success": false,
"message": "No query results for model [Campaign] 999"
}
Cause: Resource ID doesn't exist or user doesn't have access
Solution:
if (response.status === 404) {
showError('Campaign not found or no longer available');
redirectTo('/campaigns');
}
Missing Active Document
{
"success": false,
"message": "No active privacy policy found"
}
Cause: Legal document hasn't been published yet
Solution: Contact support or wait for content to be published
Rate Limit Errors (429)
{
"success": false,
"message": "Too many requests. Please try again later."
}
HTTP/1.1 429 Too Many Requests
Retry-After: 45
Cause: Exceeded rate limit (10/min for login, 120/min for general API)
Solution:
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || 60);
console.log(`Rate limited. Retrying in ${retryAfter} seconds`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
// Retry request
return fetch(url, options);
}
See Rate Limiting for prevention strategies.
Server Errors (500, 503)
Internal Server Error
{
"success": false,
"message": "Server error occurred. Please try again."
}
Cause: Unexpected server-side error
Solution:
if (response.status >= 500) {
// Log for debugging
console.error('Server error:', {
url,
status: response.status,
timestamp: new Date().toISOString()
});
// Show user-friendly message
showError('Something went wrong. Please try again in a moment.');
// Optionally retry
if (retryCount < 3) {
await wait(2000);
return retryRequest();
}
}
Service Unavailable
{
"success": false,
"message": "Service temporarily unavailable"
}
Cause: Planned maintenance or system overload
Solution: Implement exponential backoff and retry
Error Handling Patterns
1. Basic Error Handler
async function handleResponse(response) {
const data = await response.json();
if (!data.success) {
throw new APIError(data.message, response.status, data.errors);
}
return data.data;
}
class APIError extends Error {
constructor(message, status, errors = {}) {
super(message);
this.name = 'APIError';
this.status = status;
this.errors = errors;
}
}
// Usage
try {
const response = await fetch(url, options);
const data = await handleResponse(response);
console.log('Success:', data);
} catch (error) {
if (error instanceof APIError) {
console.error(`API Error ${error.status}:`, error.message);
if (error.errors) {
console.error('Validation errors:', error.errors);
}
}
}
2. Comprehensive Error Handler
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
...options.headers
}
});
const data = await response.json();
// Handle different status codes
switch (response.status) {
case 200:
case 201:
return data.data;
case 401:
// Clear auth and redirect to login
clearAuth();
redirectToLogin();
throw new Error('Session expired. Please log in again.');
case 403:
throw new Error(data.message || 'Access denied');
case 404:
throw new Error('Resource not found');
case 422:
// Validation errors
const validationError = new Error('Validation failed');
validationError.errors = data.errors;
throw validationError;
case 429:
// Rate limited - wait and retry
const retryAfter = parseInt(response.headers.get('Retry-After') || 60);
await wait(retryAfter * 1000);
return apiRequest(url, options); // Retry once
case 500:
case 503:
throw new Error('Server error. Please try again later.');
default:
throw new Error(data.message || 'Request failed');
}
} catch (error) {
// Network errors or other exceptions
if (error.name === 'TypeError') {
throw new Error('Network error. Please check your connection.');
}
throw error;
}
}
3. React Hook Pattern
function useAPI() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const request = async (url, options) => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
const data = await response.json();
if (!data.success) {
// Handle specific errors
if (response.status === 422) {
setError({
type: 'validation',
message: data.message,
fields: data.errors
});
} else {
setError({
type: 'general',
message: data.message
});
}
return null;
}
return data.data;
} catch (err) {
setError({
type: 'network',
message: 'Network error. Please try again.'
});
return null;
} finally {
setLoading(false);
}
};
return { request, loading, error };
}
// Usage in component
function DonationForm() {
const { request, loading, error } = useAPI();
const handleSubmit = async (formData) => {
const result = await request('/api/v1/donations', {
method: 'POST',
body: JSON.stringify(formData)
});
if (result) {
// Success
window.location.href = result.redirectUrl;
}
};
return (
<form onSubmit={handleSubmit}>
{error?.type === 'validation' && (
<div className="errors">
{Object.entries(error.fields).map(([field, messages]) => (
<div key={field}>{messages.join(', ')}</div>
))}
</div>
)}
{error?.type === 'general' && (
<div className="error">{error.message}</div>
)}
{/* Form fields */}
</form>
);
}
4. Axios Interceptor
import axios from 'axios';
const api = axios.create({
baseURL: 'https://batchmates-v2.revlv.com/api/v1',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
// Request interceptor - add auth token
api.interceptors.request.use(
config => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
// Response interceptor - handle errors
api.interceptors.response.use(
response => response.data.data, // Return data directly
async error => {
const { response } = error;
if (!response) {
throw new Error('Network error');
}
switch (response.status) {
case 401:
localStorage.removeItem('auth_token');
window.location.href = '/login';
break;
case 422:
// Return validation errors for form handling
throw {
type: 'validation',
message: response.data.message,
errors: response.data.errors
};
case 429:
const retryAfter = response.headers['retry-after'] || 60;
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return api.request(error.config); // Retry
default:
throw new Error(response.data.message || 'Request failed');
}
}
);
// Usage
try {
const campaigns = await api.get('/campaigns', { params: { status: 'active' } });
console.log(campaigns);
} catch (error) {
if (error.type === 'validation') {
// Handle validation errors
console.error(error.errors);
} else {
console.error(error.message);
}
}
Field Validation Errors
Common Validation Rules
- Name
required- Description
Field must be present and not empty
- Name
email- Description
Must be valid email format
- Name
min:X- Description
Minimum value (numbers) or length (strings)
- Name
max:X- Description
Maximum value (numbers) or length (strings)
- Name
in:a,b,c- Description
Must be one of the specified values
- Name
exists:table,column- Description
Must reference existing database record
- Name
unique:table,column- Description
Must be unique in database
Example: Multiple Field Errors
{
"success": false,
"message": "The given data was invalid.",
"errors": {
"email": [
"The email field is required."
],
"amount": [
"The amount must be at least 1."
],
"payment_gateway": [
"The selected payment gateway is invalid."
],
"donor_email": [
"The donor email field is required when is anonymous is false."
]
}
}
Displaying Validation Errors
function FormField({ name, label, errors }) {
const fieldErrors = errors?.[name] || [];
return (
<div className="form-field">
<label>{label}</label>
<input name={name} className={fieldErrors.length ? 'error' : ''} />
{fieldErrors.length > 0 && (
<div className="field-errors">
{fieldErrors.map((error, i) => (
<span key={i} className="error-message">{error}</span>
))}
</div>
)}
</div>
);
}
// Usage
<FormField name="email" label="Email" errors={validationErrors} />
Debugging Errors
Enable Detailed Logging
function logError(context, error, response) {
const errorLog = {
timestamp: new Date().toISOString(),
context,
status: response?.status,
statusText: response?.statusText,
url: response?.url,
message: error.message,
errors: error.errors,
stack: error.stack
};
console.error('API Error:', errorLog);
// Send to error tracking service
if (window.Sentry) {
Sentry.captureException(error, { extra: errorLog });
}
return errorLog;
}
// Usage
try {
const data = await apiRequest(url, options);
} catch (error) {
logError('donation_creation', error, error.response);
showErrorToUser(error.message);
}
Test Error Scenarios
// Test validation errors
const testValidation = async () => {
try {
await fetch('/api/v1/donations', {
method: 'POST',
body: JSON.stringify({
// Missing required fields
amount: -100 // Invalid amount
})
});
} catch (error) {
console.log('Expected validation error:', error.errors);
}
};
// Test authentication
const testAuth = async () => {
try {
await fetch('/api/v1/profile', {
headers: {
'Authorization': 'Bearer invalid_token'
}
});
} catch (error) {
console.log('Expected 401:', error.message);
}
};
Quick Reference
Status Code → Action
| Code | Meaning | User Action | Developer Action |
|---|---|---|---|
| 401 | Unauthorized | Log in again | Clear token, redirect to login |
| 403 | Forbidden | Contact support | Check user permissions |
| 404 | Not Found | Go back | Verify resource ID |
| 422 | Validation | Fix form errors | Display field errors |
| 429 | Rate Limited | Wait a moment | Implement backoff |
| 500 | Server Error | Try again later | Log and monitor |