Skip to main content

Overview

CallApi provides robust error handling through two specialized error classes: HTTPError for server-side errors and ValidationError for schema validation failures. Both error types extend the native JavaScript Error class and include detailed contextual information.

Error Types

HTTPError

Thrown when the server responds with an error status code (4xx or 5xx). Contains the response object and parsed error data.
name
string
default:"HTTPError"
Always set to "HTTPError" for type checking
message
string
Error message extracted from errorData.message, falling back to response statusText or the default HTTP error message
errorData
TErrorData
Parsed error response body from the server. Type can be customized via generics.
response
Response
The original Response object from the failed request
error.ts
class HTTPError<TErrorData = Record<string, unknown>> extends Error {
  readonly name = "HTTPError" as const;
  errorData: TErrorData;
  response: Response;

  constructor(errorDetails: HTTPErrorDetails<TErrorData>) {
    const { errorData, response, defaultHTTPErrorMessage } = errorDetails;
    
    const message = 
      errorData?.message ?? 
      defaultHTTPErrorMessage ?? 
      response.statusText ?? 
      "HTTP Error";
    
    super(message);
    this.errorData = errorData;
    this.response = response;
  }

  static isError<TErrorData>(error: unknown): error is HTTPError<TErrorData> {
    return error instanceof HTTPError;
  }
}

ValidationError

Thrown when request/response validation fails against your schema. Includes detailed information about which fields failed and why.
name
string
default:"ValidationError"
Always set to "ValidationError"
message
string
Formatted error message showing all validation issues with their paths
errorData
readonly StandardSchemaV1.Issue[]
Array of validation issues with detailed field paths and messages
issueCause
string
The schema field that caused the validation error: "body", "query", "params", "data", "errorData", etc.
response
Response | null
The Response object if validation failed on response data, otherwise null
error.ts
class ValidationError extends Error {
  readonly name = "ValidationError" as const;
  errorData: readonly StandardSchemaV1.Issue[];
  issueCause: keyof CallApiSchema | "unknown";
  response: Response | null;

  constructor(details: ValidationErrorDetails) {
    const { issueCause, issues, response } = details;
    const prettyMessage = prettifyValidationIssues(issues);
    super(`(${issueCause.toUpperCase()}) - ${prettyMessage}`);
    
    this.errorData = issues;
    this.issueCause = issueCause;
    this.response = response;
  }

  static isError(error: unknown): error is ValidationError {
    return error instanceof ValidationError;
  }
}

Usage Examples

Handling HTTPError

import { callApi } from "@zayne-labs/callapi";
import { HTTPError } from "@zayne-labs/callapi/error";

type ErrorResponse = {
  message: string;
  code: string;
  details?: Record<string, unknown>;
};

try {
  const { data } = await callApi<UserData, ErrorResponse>("/api/user/123", {
    throwOnError: true
  });
  console.log(data);
} catch (error) {
  if (HTTPError.isError<ErrorResponse>(error)) {
    console.error("HTTP Error:", error.message);
    console.error("Status:", error.response.status);
    console.error("Error code:", error.errorData.code);
    console.error("Details:", error.errorData.details);
  }
}

Handling ValidationError

import { callApi } from "@zayne-labs/callapi";
import { ValidationError } from "@zayne-labs/callapi/error";
import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(3),
  email: z.string().email(),
  age: z.number().positive()
});

try {
  const { data } = await callApi("/api/users", {
    method: "POST",
    body: { name: "Jo", email: "invalid", age: -5 },
    schema: {
      body: userSchema
    },
    throwOnError: true
  });
} catch (error) {
  if (ValidationError.isError(error)) {
    console.error("Validation failed:", error.issueCause);
    
    // Access detailed validation issues
    error.errorData.forEach(issue => {
      console.error(`- ${issue.message}`);
      if (issue.path) {
        console.error(`  at: ${issue.path.join(".")}`);
      }
    });
  }
}

Handling Both Error Types

import { callApi } from "@zayne-labs/callapi";
import { HTTPError, ValidationError } from "@zayne-labs/callapi/error";

async function fetchUser(userId: string) {
  try {
    const { data } = await callApi(`/api/users/${userId}`, {
      throwOnError: true
    });
    return data;
  } catch (error) {
    if (HTTPError.isError(error)) {
      // Handle HTTP errors (4xx, 5xx)
      if (error.response.status === 404) {
        console.error("User not found");
      } else if (error.response.status >= 500) {
        console.error("Server error:", error.message);
      }
    } else if (ValidationError.isError(error)) {
      // Handle validation errors
      console.error("Invalid response data:", error.message);
    } else {
      // Handle other errors (network, timeout, etc.)
      console.error("Request failed:", error);
    }
    throw error;
  }
}

Without Throwing (Result Object)

const result = await callApi("/api/user/123", {
  throwOnError: false // default
});

if (result.error) {
  // TypeScript knows this is an error variant
  console.error("Error name:", result.error.name);
  console.error("Error message:", result.error.message);
  
  // Check error type
  if (result.error.name === "HTTPError") {
    console.error("HTTP error data:", result.error.errorData);
    console.error("Status:", result.response?.status);
  } else if (result.error.name === "ValidationError") {
    console.error("Validation issues:", result.error.errorData);
    console.error("Failed on:", result.error.issueCause);
  }
} else {
  // TypeScript knows data is available
  console.log("Success:", result.data);
}

Custom Error Messages

Default HTTP Error Messages

Customize the default error message for HTTP errors:
const client = createFetchClient({
  baseURL: "https://api.example.com",
  defaultHTTPErrorMessage: "Request failed. Please try again."
});

// Or with a function
const client = createFetchClient({
  baseURL: "https://api.example.com",
  defaultHTTPErrorMessage: ({ response, errorData }) => {
    if (response.status === 429) {
      return "Too many requests. Please slow down.";
    }
    return errorData.message || "An error occurred";
  }
});

Per-Request Error Messages

const { data, error } = await callApi("/api/data", {
  defaultHTTPErrorMessage: "Failed to load data"
});

if (error) {
  // Will use custom message if error.errorData.message is not available
  console.error(error.message);
}

Error Handling Best Practices

Type-Safe Error Handling: Use TypeScript generics to specify your error data type for full type safety:
type ApiError = { code: string; message: string };
const result = await callApi<UserData, ApiError>("/api/user");
Always Check Error Names: Use the name property or static isError methods to identify error types. Don’t rely on instanceof checks across module boundaries.
Access Full Response: Both error types include the response object (when available), giving you access to headers, status codes, and other response metadata.

Common Error Scenarios

Network Errors

try {
  const { data } = await callApi("/api/data", {
    throwOnError: true
  });
} catch (error) {
  if (error.name === "TypeError") {
    console.error("Network error - check your connection");
  } else if (error.name === "AbortError") {
    console.error("Request was cancelled");
  } else if (error.name === "TimeoutError") {
    console.error("Request timed out");
  }
}

Abort/Timeout Errors

const controller = new AbortController();

setTimeout(() => controller.abort(), 5000);

const result = await callApi("/api/long-request", {
  signal: controller.signal
});

if (result.error?.name === "AbortError") {
  console.log("Request was aborted");
}

Build docs developers (and LLMs) love