⚡ Z-Fetch

New Features

🚨 Error Handling

Comprehensive error handling with onError hook, error mapping, and custom error modification.

Z-Fetch provides comprehensive error handling capabilities that intercept all error types and allow you to customize error messages and behavior throughout your application.

Error Types Covered

The error handling system captures:

  • HTTP errors (4xx/5xx status codes)
  • Network errors (connection failures, DNS issues)
  • Timeout errors (request timeouts → status: TIMEOUT, message: "Request timed out!")
  • Cancellation (user-initiated abort → status: CANCELED, message: "Request canceled")
  • Parse errors (invalid JSON responses)
  • Request errors (malformed requests)

Complete Coverage

The onError hook provides comprehensive error coverage and modification capabilities for better user experience.

Cancellation and Timeout

Z-Fetch standardizes cancellation and timeouts across both fetch and XHR paths:

  • Cancellation (manual abort) yields error.status = 'CANCELED' and error.message = 'Request canceled'.
  • Timeout (exceeding configured timeout) yields error.status = 'TIMEOUT' and error.message = 'Request timed out!'.
  • Error mapping does not override cancel/timeout messages by default.
// Cancel via cancelable promise
const p = api.get("/users");
setTimeout(() => p.cancel(), 10);
const r = await p; // r.error?.status === 'CANCELED'
 
// Timeout
const r2 = await api.get("/slow", { timeout: 50 });
if (r2.error?.status === "TIMEOUT") {
  // handle timeout
}

onError Hook

The onError hook intercepts all errors and allows you to modify them:

import { createInstance } from "@z-fetch/fetch";
 
const api = createInstance({
  hooks: {
    onError: (context) => {
      console.log("Error occurred:", context.error);
 
      // Access full context
      console.log("Request URL:", context.request?.url);
      console.log("Request method:", context.request?.method);
      console.log("Config:", context.config);
 
      // Modify error using helper
      context.setError({
        message: "Something went wrong. Please try again.",
        status: context.error?.status || "UNKNOWN",
        originalError: context.error,
      });
    },
  },
});

Error Context

The onError hook receives a comprehensive context object:

{
  request: {          // Request information
    url: string,
    method: string,
    options: RequestOptions
  },
  error: {            // Original error
    message: string,
    status?: number,
    name?: string,
    // ... other error properties
  },
  config: {           // Instance configuration
    baseUrl?: string,
    errorMapping?: object,
    // ... other config
  },
  setError: (error) => void  // Helper to modify error
}

Error Handling Modes

Z-Fetch supports two error handling modes to suit different coding styles:

Default Mode (throwOnError: false)

By default, errors are returned in result.error rather than thrown. This is safer and prevents unexpected crashes:

const result = await api.get("/users");
if (result.error) {
  console.error("Error:", result.error.message);
} else {
  console.log("Data:", result.data);
}

Throwing Mode (throwOnError: true)

Enable throwOnError for traditional try-catch error handling:

import { GET, createInstance } from "@z-fetch/fetch";
 
try {
  const result = await GET("/users", { throwOnError: true });
  console.log("Data:", result.data);
} catch (error) {
  console.error("Error:", error.message, error.status);
}
 
// Configure at instance level
const api = createInstance({
  baseUrl: "https://api.example.com",
  throwOnError: true,
});
 
try {
  const users = await api.get("/users");
  const posts = await api.get("/posts");
} catch (error) {
  console.error("API Error:", error.message);
}
 
// Override per request
const result = await api.get("/users", { throwOnError: false });
if (result.error) {
  // Back to default behavior
}

Choose What Fits Your Style

Default mode is safer and more explicit. Throwing mode is familiar to developers coming from fetch/axios. Both modes work with all features: error mapping, hooks, retries, and caching.

throwOnError with Retries

throwOnError works seamlessly with retry logic. Errors won't be thrown during retry attempts - only after all retries are exhausted:

const api = createInstance({
  throwOnError: true,
  retry: true,
  maxRetries: 3, // Will try: 1 original + 3 retries = 4 total attempts
});
 
try {
  // If first request fails but retry succeeds, no error is thrown
  const result = await api.get("/unstable-endpoint");
  console.log("Success:", result.data);
} catch (error) {
  // Only thrown if ALL retry attempts fail
  console.error("Failed after retries:", error.message);
}

How it works:

  • During retry attempts, errors are not thrown
  • If a retry succeeds, result.error is cleared and success data is returned
  • Errors are only thrown after all retry attempts are exhausted
  • This ensures retry logic can work properly without interruption

Error Modification

Using setError Helper

onError: (context) => {
  if (context.error?.status === 401) {
    context.setError({
      message: "Please sign in to continue",
      status: "AUTHENTICATION_REQUIRED",
      action: "LOGIN_REQUIRED",
    });
  } else if (context.error?.status === 403) {
    context.setError({
      message: "You do not have permission to access this resource",
      status: "AUTHORIZATION_FAILED",
      action: "CONTACT_ADMIN",
    });
  } else if (context.error?.status >= 500) {
    context.setError({
      message: "Server error. Please try again later.",
      status: "SERVER_ERROR",
      action: "RETRY_LATER",
    });
  }
};

Using Return Values

onError: (context) => {
  return {
    error: {
      message: "Custom error message",
      status: "CUSTOM_ERROR",
      timestamp: Date.now(),
      requestId: context.request?.headers?.["X-Request-ID"],
    },
  };
};

Error Mapping

By default, error mapping only applies to z-fetch internal errors. You can optionally enable it for backend HTTP errors.

Default: Map Z-Fetch Internal Errors Only

const api = createInstance({
  errorMapping: {
    // Network error patterns (z-fetch internal errors only)
    "fetch failed": "Network connection failed",
    "network error": "Network connection lost",
    NetworkError: "Network connection lost",
 
    // Custom patterns for network issues
    ENOTFOUND: "Unable to connect to server",
    ECONNREFUSED: "Connection refused by server",
    ENETUNREACH: "Network unreachable",
  },
});
 
// Backend errors use original statusText from your API
const result = await api.get("/users");
if (result.error) {
  console.log(result.error.message); // "Unauthorized", "Not Found", etc.
}

Optional: Map Backend HTTP Errors

Enable mapErrors: true to also map backend HTTP status codes:

const api = createInstance({
  mapErrors: true, // Enable mapping for backend errors
  errorMapping: {
    // Map backend HTTP status codes
    401: "Authentication failed - please sign in again",
    403: "Access denied - insufficient permissions",
    404: "Resource not found",
    500: "Server error - please try again later",
 
    // Also map z-fetch internal errors
    "fetch failed": "Network connection failed",
    "network error": "Unable to connect",
  },
});
 
const result = await api.get("/users");
if (result.error) {
  console.log(result.error.message); // Custom mapped message
}

Backend Error Mapping is Optional

By default (mapErrors: false), only z-fetch internal errors (NETWORK_ERROR, TIMEOUT, CANCELED) are mapped. Backend HTTP errors use the original response.statusText, allowing your backend to control error messages. Set mapErrors: true to enable mapping for backend errors too.

Pattern Matching

Error mapping supports flexible pattern matching:

const api = createInstance({
  errorMapping: {
    // Error message patterns (case-insensitive)
    "network error": "Please check your internet connection",
    "fetch failed": "Unable to reach server",
 
    // Multiple patterns for the same message
    ENOTFOUND: "Cannot connect to server",
    ECONNREFUSED: "Cannot connect to server",
    ENETUNREACH: "Cannot connect to server",
  },
});
 
// Determine error source
const result = await api.get("/users");
if (result.error) {
  // If error.status is a number (400-599), it's from your backend
  console.log(result.error.message); // Original or mapped based on mapErrors
 
  // If error.status is a string like "NETWORK_ERROR", it's from z-fetch
  // and may have been mapped via errorMapping
}

Real-World Examples

Authentication Error Handling

const api = createInstance({
  hooks: {
    onError: (context) => {
      if (context.error?.status === 401) {
        // Clear stored tokens
        localStorage.removeItem("authToken");
        localStorage.removeItem("refreshToken");
 
        // Redirect to login
        window.location.href = "/login";
 
        context.setError({
          message: "Your session has expired. Please sign in again.",
          status: "SESSION_EXPIRED",
          action: "LOGIN_REQUIRED",
        });
      }
    },
  },
});

Retry Logic with Error Handling

const api = createInstance({
  hooks: {
    onError: (context) => {
      const retryCount = context.request?.options?.retryCount || 0;
      const maxRetries = 3;
 
      // Retry for network errors and 5xx status codes
      if (retryCount < maxRetries) {
        const shouldRetry =
          context.error?.status >= 500 ||
          context.error?.message?.includes("network") ||
          context.error?.message?.includes("timeout");
 
        if (shouldRetry) {
          context.setError({
            message: `Request failed, retrying... (${retryCount + 1}/${maxRetries})`,
            status: "RETRYING",
            retryCount: retryCount + 1,
            shouldRetry: true,
          });
          return;
        }
      }
 
      // Max retries reached
      context.setError({
        message: "Request failed after multiple attempts",
        status: "MAX_RETRIES_EXCEEDED",
        attempts: retryCount + 1,
      });
    },
  },
});

User-Friendly Error Messages

const api = createInstance({
  errorMapping: {
    401: "Please sign in to continue",
    403: "You do not have permission for this action",
    404: "The requested item was not found",
    422: "Please check your input and try again",
    429: "You are making too many requests. Please wait a moment.",
    500: "Something went wrong on our end. Please try again.",
    503: "Service is temporarily unavailable. Please try again later.",
  },
 
  hooks: {
    onError: (context) => {
      // Add user-friendly context
      const baseMessage = context.error?.message || "An error occurred";
 
      context.setError({
        message: baseMessage,
        userMessage: getUserFriendlyMessage(context.error),
        timestamp: new Date().toISOString(),
        canRetry: isRetryableError(context.error),
        supportContact:
          context.error?.status >= 500 ? "support@example.com" : null,
      });
    },
  },
});
 
function getUserFriendlyMessage(error) {
  if (error?.status >= 500) {
    return "We are experiencing technical difficulties. Our team has been notified.";
  }
 
  if (error?.status >= 400 && error?.status < 500) {
    return "There was an issue with your request. Please check your input and try again.";
  }
 
  if (error?.message?.includes("network")) {
    return "Please check your internet connection and try again.";
  }
 
  return "Something unexpected happened. Please try again.";
}

Error Logging and Tracking

const api = createInstance({
  hooks: {
    onError: (context) => {
      // Log error details
      console.error("Request failed:", {
        url: context.request?.url,
        method: context.request?.method,
        status: context.error?.status,
        message: context.error?.message,
        timestamp: new Date().toISOString(),
      });
 
      // Track errors for analytics
      if (typeof gtag !== "undefined") {
        gtag("event", "api_error", {
          error_status: context.error?.status || "unknown",
          error_message: context.error?.message || "unknown",
          request_url: context.request?.url,
          request_method: context.request?.method,
        });
      }
 
      // Send to error tracking service
      if (context.error?.status >= 500) {
        sendToErrorTracking({
          error: context.error,
          request: context.request,
          userAgent: navigator.userAgent,
          timestamp: Date.now(),
        });
      }
 
      // Keep original error but add tracking
      context.setError({
        ...context.error,
        tracked: true,
        errorId: generateErrorId(),
      });
    },
  },
});

Integration with Error Mapping

The onError hook works alongside error mapping:

const api = createInstance({
  // Error mapping runs first
  errorMapping: {
    404: "Resource not found",
    500: "Internal server error",
  },
 
  hooks: {
    // onError hook runs after error mapping
    onError: (context) => {
      // The error message may already be mapped
      console.log("Mapped error message:", context.error?.message);
 
      // Add additional context
      context.setError({
        ...context.error,
        helpText: getHelpText(context.error?.status),
        timestamp: Date.now(),
      });
    },
  },
});

Error Recovery Patterns

Fallback Data

onError: (context) => {
  if (context.request?.url?.includes("/api/posts")) {
    // Provide fallback data for posts
    context.setError({
      message: "Unable to load latest posts",
      fallbackData: getCachedPosts(),
      hasFallback: true,
    });
  }
};

Graceful Degradation

onError: (context) => {
  const isFeatureRequest = context.request?.url?.includes("/features");
 
  if (isFeatureRequest) {
    context.setError({
      message: "Some features may be unavailable",
      degradedMode: true,
      availableFeatures: getBasicFeatures(),
    });
  }
};

Error Hook Best Practices

Comprehensive Logging: Always log errors for debugging User-Friendly Messages: Provide clear, actionable error messages Error Classification: Categorize errors for better handling Context Preservation: Keep original error information Recovery Options: Provide fallback data when possible Security: Don't expose sensitive error details to users

Error handling works seamlessly with other Z-Fetch features: