Tiny and elegant HTTP client based on the Fetch API
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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.
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");
}
}
};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;
}
}
};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>();
});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();
};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);
}
}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'];
}