Eloquent

Documentation

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

StatusCodeDescription
400BAD_REQUESTMalformed request body
400VALIDATION_ERRORSchema validation failed
401UNAUTHORIZEDMissing or invalid token
401MISSING_TOKENNo Authorization header
401INVALID_TOKENToken validation failed
401EXPIRED_TOKENToken has expired
403FORBIDDENAccess denied
403INSUFFICIENT_SCOPEToken lacks required scope
404NOT_FOUNDResource not found
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer error
503SERVICE_UNAVAILABLEBackend service down
504TIMEOUTRequest 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:

  1. Check route requirements in routes.yaml
  2. Add scope to gateway-token/route.ts
  3. 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:

  1. Check service logs: docker logs {service}
  2. Verify service is running: docker ps
  3. Check gateway routing configuration

Best Practices

  1. Use syserrors package - Consistent error types
  2. Include request IDs - For debugging and tracing
  3. Return meaningful messages - Help users understand issues
  4. Log errors server-side - For debugging
  5. Handle errors in UI - Show appropriate messages to users

Next Steps