Skip to content

Error Handling

All API errors follow a consistent format:

{
"error": {
"code": "SLOT_UNAVAILABLE",
"message": "This time slot is no longer available",
"details": {}
}
}
Code HTTP Status Description
UNAUTHORIZED 401 Missing or invalid session
FORBIDDEN 403 Insufficient permissions
NOT_FOUND 404 Resource not found
VALIDATION_ERROR 400 Invalid request body (Zod)
CONFLICT 409 Duplicate or conflicting state
BAD_REQUEST 400 Generic bad request
SLOT_UNAVAILABLE 409 Time slot no longer available
INSUFFICIENT_CREDITS 400 Not enough enrollment credits
POLICY_VIOLATION 403 Cancellation policy blocks action
STRIPE_ERROR 502 Stripe API failure
RATE_LIMITED 429 Too many requests
SERVICE_UNAVAILABLE 503 External service down (circuit breaker open)

Services use the Errors helper for consistent error creation:

import { Errors } from '../../lib/errors';
// Throws with correct HTTP status and error code
throw Errors.notFound('Student');
// → { code: 'NOT_FOUND', message: 'Student not found' }
throw Errors.badRequest('Invalid date range');
throw Errors.unauthorized();
throw Errors.forbidden();
throw Errors.conflict('Email already registered');

Request body validation uses Zod. Failed validation returns:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"issues": [
{
"path": ["email"],
"message": "Invalid email"
}
]
}
}
}

The API client checks for error responses and surfaces them via toast notifications:

const mutation = useMutation({
mutationFn: (input) => api.post('/teacher/sessions', input),
onError: (error) => {
toast.error(error.message || t('common.error'));
},
});