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": {}
}
}
CodeHTTP StatusDescription
UNAUTHORIZED401Missing or invalid session
FORBIDDEN403Insufficient permissions
NOT_FOUND404Resource not found
VALIDATION_ERROR400Invalid request body (Zod)
CONFLICT409Duplicate or conflicting state
BAD_REQUEST400Generic bad request
SLOT_UNAVAILABLE409Time slot no longer available
INSUFFICIENT_CREDITS400Not enough enrollment credits
POLICY_VIOLATION403Cancellation policy blocks action
STRIPE_ERROR502Stripe API failure
RATE_LIMITED429Too many requests
SERVICE_UNAVAILABLE503External 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'));
},
});