CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ky

Tiny and elegant HTTP client based on the Fetch API

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

errors.mddocs/

Error Handling

Comprehensive error handling with detailed HTTP and timeout error classes containing request and response information. Ky automatically throws errors for non-2xx responses and provides detailed error objects.

Capabilities

HTTPError Class

Thrown for HTTP responses with non-2xx status codes (when throwHttpErrors is true).

/**
 * HTTP Error class for non-2xx responses
 * Contains detailed information about the failed request and response
 */
class HTTPError<T = unknown> extends Error {
  /** The HTTP response that caused the error */
  response: KyResponse<T>;
  /** The original request */
  request: KyRequest;
  /** The normalized options used for the request */
  options: NormalizedOptions;
  /** Error name */
  name: "HTTPError";
  /** Descriptive error message with status and URL */
  message: string;
}

Usage Examples:

import ky, { HTTPError } from "ky";

try {
  const data = await ky.get("https://api.example.com/not-found").json();
} catch (error) {
  if (error instanceof HTTPError) {
    console.log("HTTP Error Details:");
    console.log("Status:", error.response.status);
    console.log("Status Text:", error.response.statusText);
    console.log("URL:", error.request.url);
    console.log("Method:", error.request.method);
    console.log("Message:", error.message);
    
    // Access response body for more details
    const errorBody = await error.response.text();
    console.log("Response Body:", errorBody);
    
    // Check specific status codes
    if (error.response.status === 404) {
      console.log("Resource not found");
    } else if (error.response.status === 401) {
      console.log("Authentication required");
    } else if (error.response.status >= 500) {
      console.log("Server error");
    }
  }
}

// Handle different error types
const handleApiCall = async () => {
  try {
    return await ky.post("https://api.example.com/data", {
      json: { value: "test" }
    }).json();
  } catch (error) {
    if (error instanceof HTTPError) {
      // HTTP errors (4xx, 5xx responses)
      switch (error.response.status) {
        case 400:
          throw new Error("Invalid request data");
        case 401:
          throw new Error("Please log in to continue");
        case 403:
          throw new Error("You don't have permission for this action");
        case 404:
          throw new Error("The requested resource was not found");
        case 429:
          throw new Error("Rate limit exceeded. Please try again later");
        case 500:
          throw new Error("Server error. Please try again later");
        default:
          throw new Error(`Request failed with status ${error.response.status}`);
      }
    } else {
      // Network errors, timeouts, etc.
      throw new Error("Network error. Please check your connection");
    }
  }
};

TimeoutError Class

Thrown when requests exceed the configured timeout duration.

/**
 * Timeout Error class for requests that exceed timeout duration
 * Contains information about the request that timed out
 */
class TimeoutError extends Error {
  /** The original request that timed out */
  request: KyRequest;
  /** Error name */
  name: "TimeoutError";
  /** Descriptive error message with method and URL */
  message: string;
}

Usage Examples:

import ky, { TimeoutError } from "ky";

try {
  const data = await ky.get("https://api.example.com/slow-endpoint", {
    timeout: 5000 // 5 second timeout
  }).json();
} catch (error) {
  if (error instanceof TimeoutError) {
    console.log("Request timed out:");
    console.log("URL:", error.request.url);
    console.log("Method:", error.request.method);
    console.log("Message:", error.message);
    
    // Handle timeout specifically
    throw new Error("The request took too long. Please try again");
  }
}

// Timeout handling with retry logic
const fetchWithTimeoutHandling = async (url: string, maxAttempts = 3) => {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await ky.get(url, {
        timeout: 10000 // 10 seconds
      }).json();
    } catch (error) {
      if (error instanceof TimeoutError) {
        console.log(`Attempt ${attempt} timed out`);
        
        if (attempt === maxAttempts) {
          throw new Error("Request failed: Multiple timeout attempts");
        }
        
        // Wait before retrying
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
        continue;
      }
      
      // Re-throw non-timeout errors immediately
      throw error;
    }
  }
};

Error Handling Patterns

Common patterns for handling different types of errors in applications.

Usage Examples:

import ky, { HTTPError, TimeoutError } from "ky";

// Comprehensive error handling function
const safeApiCall = async <T>(
  url: string,
  options?: any
): Promise<{ data?: T; error?: string }> => {
  try {
    const data = await ky(url, options).json<T>();
    return { data };
  } catch (error) {
    if (error instanceof HTTPError) {
      // Handle HTTP errors
      const status = error.response.status;
      
      if (status >= 400 && status < 500) {
        // Client errors
        return { error: `Client error: ${status}` };
      } else if (status >= 500) {
        // Server errors
        return { error: "Server error. Please try again later" };
      }
    } else if (error instanceof TimeoutError) {
      // Handle timeouts
      return { error: "Request timeout. Please try again" };
    } else {
      // Handle other errors (network, etc.)
      return { error: "Network error. Please check your connection" };
    }
    
    return { error: "An unexpected error occurred" };
  }
};

// Usage of safe API call
const loadUserData = async (userId: string) => {
  const { data, error } = await safeApiCall<User>(`/api/users/${userId}`);
  
  if (error) {
    showErrorMessage(error);
    return null;
  }
  
  return data;
};

// Error boundary for React-like patterns
const withErrorHandling = <T extends any[], R>(
  fn: (...args: T) => Promise<R>
) => {
  return async (...args: T): Promise<R | null> => {
    try {
      return await fn(...args);
    } catch (error) {
      if (error instanceof HTTPError) {
        // Log HTTP errors with context
        console.error("HTTP Error:", {
          url: error.request.url,
          method: error.request.method,
          status: error.response.status,
          statusText: error.response.statusText
        });
        
        // Get response body for debugging
        try {
          const body = await error.response.clone().text();
          console.error("Response body:", body);
        } catch {
          // Ignore if body can't be read
        }
      } else if (error instanceof TimeoutError) {
        console.error("Timeout Error:", {
          url: error.request.url,
          method: error.request.method
        });
      } else {
        console.error("Unexpected error:", error);
      }
      
      return null;
    }
  };
};

// Wrapped API functions
const getUser = withErrorHandling(async (id: string) => {
  return ky.get(`/api/users/${id}`).json<User>();
});

const createUser = withErrorHandling(async (userData: CreateUserData) => {
  return ky.post("/api/users", { json: userData }).json<User>();
});

Disabling Automatic Error Throwing

Configure ky to not throw errors for non-2xx responses.

Usage Examples:

import ky from "ky";

// Disable error throwing globally
const tolerantClient = ky.create({
  throwHttpErrors: false
});

// Handle responses manually
const response = await tolerantClient.get("https://api.example.com/data");

if (response.ok) {
  const data = await response.json();
  console.log("Success:", data);
} else {
  console.log("Error:", response.status, response.statusText);
  
  // Get error details from response
  const errorText = await response.text();
  console.log("Error details:", errorText);
}

// Per-request error handling
const checkResourceExists = async (url: string): Promise<boolean> => {
  const response = await ky.get(url, {
    throwHttpErrors: false
  });
  
  return response.ok;
};

// Manual error handling with detailed information
const manualErrorHandling = async () => {
  const response = await ky.post("https://api.example.com/submit", {
    json: { data: "test" },
    throwHttpErrors: false
  });
  
  if (!response.ok) {
    // Handle different status codes manually
    switch (response.status) {
      case 400:
        const validationErrors = await response.json();
        console.log("Validation errors:", validationErrors);
        break;
      case 401:
        console.log("Authentication required");
        // Redirect to login
        break;
      case 403:
        console.log("Access forbidden");
        break;
      case 404:
        console.log("Resource not found");
        break;
      case 429:
        const retryAfter = response.headers.get("Retry-After");
        console.log(`Rate limited. Retry after: ${retryAfter}`);
        break;
      case 500:
        console.log("Internal server error");
        break;
      default:
        console.log(`Unexpected error: ${response.status}`);
    }
    
    return null;
  }
  
  return response.json();
};

Custom Error Enhancement

Use hooks to enhance errors with additional information.

Usage Examples:

import ky, { HTTPError } from "ky";

// Client with enhanced error messages
const enhancedClient = ky.create({
  hooks: {
    beforeError: [
      async (error) => {
        // Add response body to error message
        try {
          const responseBody = await error.response.clone().text();
          if (responseBody) {
            error.message += `\nResponse: ${responseBody}`;
          }
        } catch {
          // Ignore if response body can't be read
        }
        
        // Add request details
        error.message += `\nRequest: ${error.request.method} ${error.request.url}`;
        
        // Add headers for debugging
        const headers = Object.fromEntries(error.request.headers);
        error.message += `\nHeaders: ${JSON.stringify(headers, null, 2)}`;
        
        return error;
      }
    ]
  }
});

// Custom error class
class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public code?: string,
    public details?: any
  ) {
    super(message);
    this.name = "ApiError";
  }
}

// Client that transforms HTTPError to custom error
const customErrorClient = ky.create({
  hooks: {
    beforeError: [
      async (error) => {
        try {
          const errorData = await error.response.clone().json();
          
          // Transform to custom error if response has expected format
          if (errorData.message || errorData.error) {
            const customError = new ApiError(
              errorData.message || errorData.error,
              error.response.status,
              errorData.code,
              errorData.details
            );
            
            // Preserve original error properties
            (customError as any).response = error.response;
            (customError as any).request = error.request;
            (customError as any).options = error.options;
            
            return customError as any;
          }
        } catch {
          // Fall back to original error if transformation fails
        }
        
        return error;
      }
    ]
  }
});

// Usage with custom error
try {
  await customErrorClient.post("/api/data", {
    json: { invalid: "data" }
  }).json();
} catch (error) {
  if (error instanceof ApiError) {
    console.log("API Error:", error.message);
    console.log("Status:", error.status);
    console.log("Code:", error.code);
    console.log("Details:", error.details);
  } else if (error instanceof HTTPError) {
    console.log("HTTP Error:", error.message);
  }
}

Types

class HTTPError<T = unknown> extends Error {
  response: KyResponse<T>;
  request: KyRequest;
  options: NormalizedOptions;
  name: "HTTPError";
}

class TimeoutError extends Error {
  request: KyRequest;
  name: "TimeoutError";
}

interface KyResponse<T = unknown> extends Response {
  json<J = T>(): Promise<J>;
}

interface KyRequest<T = unknown> extends Request {
  json<J = T>(): Promise<J>;
}

interface NormalizedOptions extends RequestInit {
  method: NonNullable<RequestInit['method']>;
  credentials?: NonNullable<RequestInit['credentials']>;
  retry: RetryOptions;
  prefixUrl: string;
  onDownloadProgress: Options['onDownloadProgress'];
  onUploadProgress: Options['onUploadProgress'];
}

docs

configuration.md

errors.md

hooks.md

http-methods.md

index.md

instances.md

responses.md

retry.md

tile.json