chowbea-axios
Usage

Error Handling

Result-based error handling with normalized error messages.

chowbea-axios uses Result-based error handling—API calls return { data, error } instead of throwing exceptions.

The Result Type

Every API call returns a Result<T>:

type Result<T> =
  | { data: T; error: null }        // Success
  | { data: null; error: ApiError }; // Failure

This makes error handling explicit and type-safe.

Basic Error Handling

import { api } from "./api/api.client";

const { data, error } = await api.get("/users/{id}", { id: "123" });

if (error) {
  // error is ApiError, data is null
  console.error("Request failed:", error.message);
  return;
}

// TypeScript knows data is not null here
console.log("User:", data.name);

ApiError Structure

The ApiError type contains:

interface ApiError {
  /** Human-readable error message */
  message: string;
  
  /** Error code (NETWORK_ERROR, NOT_FOUND, etc.) */
  code: string;
  
  /** HTTP status code (null for network errors) */
  status: number | null;
  
  /** Request context for debugging */
  request: RequestContext;
  
  /** Original error response body */
  details?: unknown;
}

interface RequestContext {
  method: string;    // "GET", "POST", etc.
  url: string;       // "/users/123"
  baseURL?: string;  // "https://api.example.com"
  params?: unknown;  // Query parameters
  data?: unknown;    // Request body (sensitive fields redacted)
}

Error Codes

CodeHTTP StatusDescription
NETWORK_ERRORnullNetwork failure, no response
TIMEOUTnullRequest timed out
BAD_REQUEST400Invalid request
UNAUTHORIZED401Authentication required
FORBIDDEN403Permission denied
NOT_FOUND404Resource not found
CONFLICT409Resource conflict
VALIDATION_ERROR422Validation failed
RATE_LIMITED429Too many requests
SERVER_ERROR5xxServer error
REQUEST_ERRORotherOther client errors
UNKNOWN_ERROR-Unexpected error

Handling Specific Errors

const { data, error } = await api.get("/users/{id}", { id: "123" });

if (error) {
  switch (error.code) {
    case "NOT_FOUND":
      console.log("User not found");
      return null;
      
    case "UNAUTHORIZED":
      // Redirect to login
      window.location.href = "/login";
      return;
      
    case "NETWORK_ERROR":
      console.log("Network issue, retrying...");
      // Implement retry logic
      break;
      
    default:
      console.error("Unexpected error:", error.message);
      throw new Error(error.message);
  }
}

Type Guards

Use the provided type guards for cleaner code:

import { api, isSuccess, isError } from "./api/api.client";

const result = await api.get("/users/{id}", { id: "123" });

if (isSuccess(result)) {
  // result.data is typed, result.error is null
  console.log(result.data.name);
}

if (isError(result)) {
  // result.error is ApiError, result.data is null
  console.error(result.error.message);
}

Error Normalization

The client automatically extracts error messages from various API response formats:

// These all produce the same error.message:

// Format 1: { message: "User not found" }
// Format 2: { error: "User not found" }
// Format 3: { error: { message: "User not found" } }
// Format 4: { errors: ["User not found"] }
// Format 5: { errors: { email: ["Invalid email"] } }
// Format 6: { detail: "User not found" }  // FastAPI
// Format 7: { title: "User not found" }   // ASP.NET Problem Details

This normalization means you don't need to handle different backend error formats.

Accessing Raw Response

The original response data is available in error.details:

if (error) {
  console.log("Normalized message:", error.message);
  console.log("Raw response:", error.details);
  
  // For validation errors, details might contain field errors
  if (error.code === "VALIDATION_ERROR" && error.details) {
    const details = error.details as { errors: Record<string, string[]> };
    for (const [field, messages] of Object.entries(details.errors)) {
      console.log(`${field}: ${messages.join(", ")}`);
    }
  }
}

Request Context for Debugging

Every error includes context about the failed request:

if (error) {
  console.log("Failed request:");
  console.log("  Method:", error.request.method);
  console.log("  URL:", error.request.url);
  console.log("  Params:", error.request.params);
  
  // Note: Sensitive fields in request body are redacted
  console.log("  Body:", error.request.data);
}

Creating Custom Errors

You can create ApiError objects manually if needed:

import { createApiError } from "./api/api.client";

try {
  // Some operation that might throw
} catch (err) {
  const apiError = createApiError(err);
  // apiError is now a normalized ApiError
}

Wrapper for Throwing Behavior

If you prefer try/catch, create a wrapper:

import { api, type ApiError } from "./api/api.client";

class ApiException extends Error {
  constructor(public apiError: ApiError) {
    super(apiError.message);
    this.name = "ApiException";
  }
}

async function fetchOrThrow<T>(
  promise: Promise<{ data: T | null; error: ApiError | null }>
): Promise<T> {
  const { data, error } = await promise;
  if (error) throw new ApiException(error);
  return data;
}

// Usage
try {
  const user = await fetchOrThrow(api.get("/users/{id}", { id: "123" }));
  console.log(user.name);
} catch (err) {
  if (err instanceof ApiException) {
    console.error(err.apiError.code);
  }
}

React Error Boundary Integration

React component
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<ApiError | null>(null);

  useEffect(() => {
    async function loadUser() {
      const { data, error } = await api.get("/users/{id}", { id: userId });
      
      if (error) {
        setError(error);
        return;
      }
      
      setUser(data);
    }
    
    loadUser();
  }, [userId]);

  if (error) {
    return <ErrorDisplay error={error} />;
  }

  if (!user) {
    return <Loading />;
  }

  return <div>{user.name}</div>;
}

function ErrorDisplay({ error }: { error: ApiError }) {
  if (error.code === "NOT_FOUND") {
    return <div>User not found</div>;
  }
  
  if (error.code === "UNAUTHORIZED") {
    return <div>Please log in to view this page</div>;
  }
  
  return <div>Error: {error.message}</div>;
}

Next Steps

On this page