Error Handling
This guide covers the unified error handling patterns used across the Eloquent platform, ensuring consistent error responses from backend to frontend.
API Response Format
All backend services return responses using a unified format:
interface APIResponse<T = any> {
data?: T; // Success payload
error?: APIError; // Error details (if failed)
metadata?: ResponseMetadata; // Response metadata
}
interface APIError {
code: string; // Error code (e.g., "VALIDATION_ERROR")
message: string; // Human-readable message
details?: Record<string, any>; // Additional error details
request_id?: string; // Request ID for debugging
}
interface ResponseMetadata {
success: boolean;
timestamp: number;
message?: string;
error?: string;
requestId?: string;
}
Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | Malformed request body |
| 400 | VALIDATION_ERROR | Schema validation failed |
| 401 | UNAUTHORIZED | Missing or invalid token |
| 401 | MISSING_TOKEN | No Authorization header |
| 401 | INVALID_TOKEN | Token validation failed |
| 401 | EXPIRED_TOKEN | Token has expired |
| 403 | FORBIDDEN | Access denied |
| 403 | INSUFFICIENT_SCOPE | Token lacks required scope |
| 404 | NOT_FOUND | Resource not found |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Server error |
| 503 | SERVICE_UNAVAILABLE | Backend service down |
| 504 | TIMEOUT | Request timeout |
Backend Error Handling
Using syserrors Package
import "blazi/common/syserrors"
func (h *Handler) GetWidget(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
widget, err := h.service.Get(r.Context(), id)
if err != nil {
// Use syserrors for typed errors
if errors.Is(err, syserrors.ErrNotFound) {
httputil.HandleError(w, syserrors.NewNotFoundError("widget", id))
return
}
httputil.HandleError(w, err)
return
}
httputil.WriteJSON(w, http.StatusOK, widget)
}
Creating Typed Errors
// Validation error
err := syserrors.NewValidationError("email", "invalid email format")
// Not found error
err := syserrors.NewNotFoundError("widget", id)
// Permission error
err := syserrors.NewForbiddenError("delete widget")
// Custom error
err := syserrors.NewError("CUSTOM_ERROR", "Something went wrong", details)
httputil.HandleError
func HandleError(w http.ResponseWriter, err error) {
var sysErr *syserrors.Error
if errors.As(err, &sysErr) {
writeJSON(w, sysErr.HTTPStatus(), APIResponse{
Error: &APIError{
Code: sysErr.Code,
Message: sysErr.Message,
Details: sysErr.Details,
},
Metadata: &ResponseMetadata{
Success: false,
},
})
return
}
// Unknown error - return 500
writeJSON(w, http.StatusInternalServerError, APIResponse{
Error: &APIError{
Code: "INTERNAL_ERROR",
Message: "An unexpected error occurred",
},
Metadata: &ResponseMetadata{
Success: false,
},
})
}
Frontend Error Handling
API Client
export async function apiRequest<T>(
endpoint: string,
options: ApiRequestOptions
): Promise<ApiResponse<T>> {
try {
const response = await fetch(url, fetchOptions);
const data = await response.json();
// Check HTTP-level error
if (!response.ok) {
const error = data.error || data;
return {
error: error.message || `HTTP ${response.status}`,
code: error.code,
status: response.status,
};
}
// Check application-level error
if (data.error) {
return {
error: data.error.message,
code: data.error.code,
details: data.error.details,
status: response.status,
};
}
// Success
return { data, status: response.status };
} catch (error) {
return {
error: error instanceof Error ? error.message : "Network error",
status: 0,
};
}
}
Error Handling in Components
import { ErrorCodes } from "@/types/api";
export function AgentsList() {
const { agents, error, code, isLoading, refresh } = useAgentsContext();
if (isLoading) return <Spinner />;
if (error) {
switch (code) {
case ErrorCodes.UNAUTHORIZED:
case ErrorCodes.EXPIRED_TOKEN:
return <LoginPrompt message="Please sign in again" />;
case ErrorCodes.INSUFFICIENT_SCOPE:
return <AccessDenied message="You don't have permission" />;
case ErrorCodes.RATE_LIMITED:
return <RateLimitedMessage onRetry={refresh} />;
case ErrorCodes.SERVICE_UNAVAILABLE:
return <ServiceDown onRetry={refresh} />;
default:
return <ErrorMessage message={error} onRetry={refresh} />;
}
}
return (
<ul>
{agents.map(agent => <AgentItem key={agent.id} agent={agent} />)}
</ul>
);
}
Error Types
export const ErrorCodes = {
UNAUTHORIZED: "UNAUTHORIZED",
MISSING_TOKEN: "MISSING_TOKEN",
INVALID_TOKEN: "INVALID_TOKEN",
EXPIRED_TOKEN: "EXPIRED_TOKEN",
FORBIDDEN: "FORBIDDEN",
INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE",
NOT_FOUND: "NOT_FOUND",
BAD_REQUEST: "BAD_REQUEST",
VALIDATION_ERROR: "VALIDATION_ERROR",
RATE_LIMITED: "RATE_LIMITED",
INTERNAL_ERROR: "INTERNAL_ERROR",
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
TIMEOUT: "TIMEOUT",
} as const;
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
Troubleshooting Common Errors
403 INSUFFICIENT_SCOPE
Cause: JWT token doesn't have the required scope.
Fix:
- Check route requirements in
routes.yaml - Add scope to
gateway-token/route.ts - Clear token cache (hard refresh browser)
401 EXPIRED_TOKEN
Cause: JWT token has expired.
Fix: Token refresh should happen automatically. If persisting, check token generation.
503 SERVICE_UNAVAILABLE
Cause: Backend service is down or unreachable.
Fix:
- Check service logs:
docker logs {service} - Verify service is running:
docker ps - Check gateway routing configuration
Best Practices
- Use syserrors package - Consistent error types
- Include request IDs - For debugging and tracing
- Return meaningful messages - Help users understand issues
- Log errors server-side - For debugging
- Handle errors in UI - Show appropriate messages to users
Next Steps
- API Gateway - Gateway error handling
- Backend Services - Service implementation
- Frontend Apps - UI error handling