Universal HTTP fetch library with intelligent parsing, error handling, retry logic, and cross-environment compatibility
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Enhanced error objects with request/response context, automatic retry for specific status codes, configurable retry strategies, and comprehensive error information for debugging and monitoring.
Enhanced error class that provides comprehensive information about fetch failures including request details, response data, and contextual information.
/**
* Enhanced error class for fetch failures
* Provides comprehensive context about the failed request
*/
class FetchError<T = any> extends Error implements IFetchError<T> {
name: "FetchError";
request?: FetchRequest;
options?: FetchOptions;
response?: FetchResponse<T>;
data?: T;
status?: number;
statusText?: string;
statusCode?: number; // Alias for status
statusMessage?: string; // Alias for statusText
cause?: unknown;
}
interface IFetchError<T = any> extends Error {
request?: FetchRequest;
options?: FetchOptions;
response?: FetchResponse<T>;
data?: T;
status?: number;
statusText?: string;
statusCode?: number;
statusMessage?: string;
}Usage Examples:
import { ofetch, FetchError } from "ofetch";
try {
const data = await ofetch("https://api.example.com/users/999");
} catch (error) {
if (error instanceof FetchError) {
console.log("Request:", error.request);
console.log("Status:", error.status); // 404
console.log("Status Text:", error.statusText); // "Not Found"
console.log("Response Data:", error.data); // Parsed error response
console.log("Original Options:", error.options);
// Handle specific error types
if (error.status === 404) {
console.log("Resource not found");
} else if (error.status >= 500) {
console.log("Server error");
}
}
}
// Access error data for API error messages
try {
await ofetch("/api/users", { method: "POST", body: {} });
} catch (error) {
if (error instanceof FetchError && error.data) {
console.log("API Error:", error.data.message);
console.log("Validation Errors:", error.data.errors);
}
}Factory function for creating FetchError instances with proper context population.
/**
* Factory function to create FetchError instances
* @param ctx - Fetch context containing request, response, and error details
* @returns Enhanced FetchError with populated context
*/
function createFetchError<T = any>(ctx: FetchContext<T>): IFetchError<T>;ofetch automatically retries requests for specific HTTP status codes with configurable retry counts and delays.
interface FetchOptions {
/** Number of retry attempts, or false to disable retry */
retry?: number | false;
/** Delay between retries in milliseconds, or function for custom delay calculation */
retryDelay?: number | ((context: FetchContext<T, R>) => number);
/** HTTP status codes that trigger retries (default: [408, 409, 425, 429, 500, 502, 503, 504]) */
retryStatusCodes?: number[];
}Default Retry Status Codes:
408 - Request Timeout409 - Conflict425 - Too Early (Experimental)429 - Too Many Requests500 - Internal Server Error502 - Bad Gateway503 - Service Unavailable504 - Gateway TimeoutUsage Examples:
import { ofetch } from "ofetch";
// Default retry behavior (1 retry for GET, 0 for POST/PUT/PATCH/DELETE)
const data = await ofetch("https://unreliable-api.com/data");
// Custom retry configuration
const data = await ofetch("https://api.example.com/data", {
retry: 3,
retryDelay: 1000, // 1 second between retries
retryStatusCodes: [408, 429, 500, 502, 503] // Custom retry codes
});
// Exponential backoff retry delay
const data = await ofetch("https://api.example.com/data", {
retry: 5,
retryDelay: (context) => {
const attempt = (context.options.retry || 0) - ((context.options as any)._retryCount || 0);
return Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30 seconds
}
});
// Disable retry for specific request
const data = await ofetch("https://api.example.com/data", {
retry: false
});
// Force retry for POST requests (normally no retry)
const result = await ofetch("https://api.example.com/submit", {
method: "POST",
body: { data: "value" },
retry: 2 // Override default no-retry for POST
});Control whether ofetch throws errors for HTTP error status codes.
interface FetchOptions {
/** Skip throwing errors for 4xx/5xx status codes */
ignoreResponseError?: boolean;
}Usage Examples:
import { ofetch } from "ofetch";
// Handle errors manually without throwing
const response = await ofetch.raw("https://api.example.com/might-fail", {
ignoreResponseError: true
});
if (response.status >= 400) {
console.log("Error:", response.status, response._data);
} else {
console.log("Success:", response._data);
}
// Conditional error handling
const data = await ofetch("https://api.example.com/users/999", {
ignoreResponseError: true
});
// For regular ofetch calls with ignoreResponseError, check manually
try {
const data = await ofetch("https://api.example.com/data", {
ignoreResponseError: true
});
// No error thrown, but data might be error response
if (data && data.error) {
console.log("API returned error:", data.error);
}
} catch (error) {
// Network or other non-HTTP errors still throw
console.log("Network error:", error.message);
}Comprehensive error context for debugging and monitoring.
Usage Examples:
import { ofetch, FetchError } from "ofetch";
// Comprehensive error logging
try {
const data = await ofetch("https://api.example.com/data", {
method: "POST",
body: { invalid: "data" },
headers: { "Content-Type": "application/json" }
});
} catch (error) {
if (error instanceof FetchError) {
// Log full error context
console.error("Fetch Error Details:", {
message: error.message,
url: error.request,
method: error.options?.method,
status: error.status,
statusText: error.statusText,
requestHeaders: error.options?.headers,
responseHeaders: error.response?.headers,
requestBody: error.options?.body,
responseBody: error.data,
timestamp: new Date().toISOString()
});
}
}
// Error categorization for monitoring
function categorizeError(error: FetchError) {
if (!error.status) return "network_error";
if (error.status >= 400 && error.status < 500) return "client_error";
if (error.status >= 500) return "server_error";
return "unknown_error";
}
try {
await ofetch("https://api.example.com/data");
} catch (error) {
if (error instanceof FetchError) {
const category = categorizeError(error);
metrics.increment(`http_error.${category}`, {
status: error.status,
endpoint: error.request
});
}
}Configure request timeouts with automatic AbortController integration.
interface FetchOptions {
/** Timeout in milliseconds */
timeout?: number;
}Usage Examples:
import { ofetch } from "ofetch";
// Request timeout
try {
const data = await ofetch("https://slow-api.com/data", {
timeout: 5000 // 5 second timeout
});
} catch (error) {
if (error.name === "TimeoutError") {
console.log("Request timed out after 5 seconds");
}
}
// Different timeouts for different operations
const api = ofetch.create({
baseURL: "https://api.example.com",
timeout: 10000 // Default 10 second timeout
});
// Override timeout for specific requests
const quickData = await api("/quick-endpoint", { timeout: 2000 });
const slowData = await api("/slow-endpoint", { timeout: 30000 });Common patterns for handling different types of errors in applications.
Usage Examples:
import { ofetch, FetchError } from "ofetch";
// Wrapper function with comprehensive error handling
async function apiRequest<T>(url: string, options?: any): Promise<T> {
try {
return await ofetch<T>(url, options);
} catch (error) {
if (error instanceof FetchError) {
// Handle specific HTTP errors
switch (error.status) {
case 401:
throw new Error("Authentication required");
case 403:
throw new Error("Access denied");
case 404:
throw new Error("Resource not found");
case 429:
throw new Error("Rate limit exceeded");
case 500:
throw new Error("Server error - please try again later");
default:
throw new Error(`Request failed: ${error.message}`);
}
}
// Handle network errors
if (error.name === "TimeoutError") {
throw new Error("Request timeout - please check your connection");
}
throw error; // Re-throw unknown errors
}
}
// Retry with custom error handling
async function retryableRequest<T>(url: string, maxRetries = 3): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await ofetch<T>(url, {
timeout: 5000 * attempt, // Increasing timeout
retry: false // Handle retry manually
});
} catch (error) {
lastError = error;
if (error instanceof FetchError) {
// Don't retry client errors (4xx)
if (error.status && error.status >= 400 && error.status < 500) {
throw error;
}
}
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
throw lastError!;
}
// Global error handler with logging
const api = ofetch.create({
baseURL: "https://api.example.com",
onResponseError({ request, response, error }) {
// Log all API errors
logger.error("API Error", {
url: request,
status: response.status,
data: response._data,
timestamp: new Date().toISOString()
});
}
});Install with Tessl CLI
npx tessl i tessl/npm-ofetch