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 errors object for details.

  • Name
    429 Too Many Requests
    Description

    Rate limit exceeded. Wait before retrying (see Retry-After header).

  • 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 false for 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

CodeMeaningUser ActionDeveloper Action
401UnauthorizedLog in againClear token, redirect to login
403ForbiddenContact supportCheck user permissions
404Not FoundGo backVerify resource ID
422ValidationFix form errorsDisplay field errors
429Rate LimitedWait a momentImplement backoff
500Server ErrorTry again laterLog and monitor

Was this page helpful?