Error Handling
Error Handling
@shkumbinhsn/fetcher provides structured error handling through custom error classes. This allows you to handle different types of API errors in a type-safe way.
Defining Error Classes
Use defineError() to create custom error classes that match your API’s error responses:
import { defineError } from '@shkumbinhsn/fetcher';import { z } from 'zod';
// Define a 404 Not Found errorconst NotFoundError = defineError( 404, // HTTP status code z.object({ // Error response schema message: z.string(), resource: z.string(), code: z.string() }), 'NotFoundError' // Error class name (optional));
// Define a 400 Validation errorconst ValidationError = defineError( 400, z.object({ message: z.string(), errors: z.array(z.object({ field: z.string(), message: z.string(), code: z.string() })) }), 'ValidationError');
// Define a 401 Unauthorized errorconst UnauthorizedError = defineError( 401, z.object({ message: z.string(), code: z.string() }), 'UnauthorizedError');Using Error Classes
Pass error classes to the fetcher function:
import { fetcher } from '@shkumbinhsn/fetcher';
try { const user = await fetcher('/api/users/999', { schema: UserSchema, errors: [NotFoundError, ValidationError, UnauthorizedError] });} catch (error) { // Handle specific error types if (error instanceof NotFoundError) { console.log(`Resource not found: ${error.data.resource}`); console.log(`Error code: ${error.data.code}`); } else if (error instanceof ValidationError) { console.log('Validation failed:'); error.data.errors.forEach(err => { console.log(` ${err.field}: ${err.message}`); }); } else if (error instanceof UnauthorizedError) { console.log('Authentication required'); // Redirect to login window.location.href = '/login'; } else { console.log('Unexpected error:', error); }}Error Class Properties
Custom error classes extend the base ApiError class and provide:
class CustomError extends ApiError { statusCode: number; // HTTP status code (e.g., 404) data: T; // Validated error response data response: Response; // Original fetch Response object message: string; // Error message name: string; // Error class name}Accessing Error Details
try { await fetcher('/api/users/999', { errors: [NotFoundError] });} catch (error) { if (error instanceof NotFoundError) { // All properties are type-safe console.log('Status:', error.statusCode); // 404 console.log('Message:', error.message); // "Not Found" (from response) console.log('Resource:', error.data.resource); // Type-safe access console.log('Headers:', error.response.headers); // Access original response }}Common Error Patterns
REST API Errors
// Standard REST API error responsesconst BadRequestError = defineError(400, z.object({ message: z.string(), details: z.record(z.any()).optional()}));
const ForbiddenError = defineError(403, z.object({ message: z.string(), requiredPermissions: z.array(z.string()).optional()}));
const ConflictError = defineError(409, z.object({ message: z.string(), conflictingResource: z.string()}));
const RateLimitError = defineError(429, z.object({ message: z.string(), retryAfter: z.number(), limit: z.number(), remaining: z.number()}));
const ServerError = defineError(500, z.object({ message: z.string(), errorId: z.string(), timestamp: z.string()}));GraphQL-style Errors
const GraphQLError = defineError(400, z.object({ errors: z.array(z.object({ message: z.string(), locations: z.array(z.object({ line: z.number(), column: z.number() })).optional(), path: z.array(z.union([z.string(), z.number()])).optional(), extensions: z.record(z.any()).optional() }))}));Form Validation Errors
const FormValidationError = defineError(422, z.object({ message: z.string(), errors: z.record(z.array(z.string())) // field -> array of error messages}));
// Usagetry { await fetcher('/api/users', { method: 'POST', body: JSON.stringify(formData), errors: [FormValidationError] });} catch (error) { if (error instanceof FormValidationError) { Object.entries(error.data.errors).forEach(([field, messages]) => { messages.forEach(message => { console.log(`${field}: ${message}`); }); }); }}Multiple Status Codes
Handle the same error schema for multiple status codes:
// Create separate error classes for different status codesconst BadRequestValidation = defineError(400, ValidationSchema);const UnprocessableValidation = defineError(422, ValidationSchema);
// Use both in requestsconst errors = [BadRequestValidation, UnprocessableValidation];
try { await fetcher('/api/users', { errors });} catch (error) { if (error instanceof BadRequestValidation || error instanceof UnprocessableValidation) { // Handle validation errors from either status code console.log('Validation failed:', error.data.errors); }}Error Middleware
Create reusable error handling logic:
function handleApiError(error: unknown): never { if (error instanceof NotFoundError) { throw new Error(`Resource not found: ${error.data.resource}`); }
if (error instanceof ValidationError) { const messages = error.data.errors.map(e => `${e.field}: ${e.message}`); throw new Error(`Validation failed: ${messages.join(', ')}`); }
if (error instanceof UnauthorizedError) { // Clear auth state localStorage.removeItem('token'); window.location.href = '/login'; throw new Error('Authentication required'); }
if (error instanceof RateLimitError) { throw new Error(`Rate limit exceeded. Retry after ${error.data.retryAfter} seconds`); }
// Re-throw unknown errors throw error;}
// Use in your API callstry { const user = await fetcher('/api/users/123', { schema: UserSchema, errors: [NotFoundError, ValidationError, UnauthorizedError, RateLimitError] });} catch (error) { handleApiError(error);}Default Error Handling
When no custom errors are provided, the library falls back to generic error messages:
try { await fetcher('/api/users/999'); // No custom errors} catch (error) { // Will throw a generic Error with message like: // "Request failed: 404 Not Found" console.log(error.message);}Error Logging
Add comprehensive error logging:
function logApiError(error: unknown, context: string) { if (error instanceof ApiError) { console.error(`API Error in ${context}:`, { statusCode: error.statusCode, message: error.message, data: error.data, url: error.response.url, timestamp: new Date().toISOString() }); } else { console.error(`Unexpected error in ${context}:`, error); }}
try { await fetcher('/api/users/123', { errors: [NotFoundError] });} catch (error) { logApiError(error, 'getUserById'); throw error;}Testing Error Handling
Test your error handling logic:
// Mock API response for testingconst mockNotFoundResponse = { message: 'User not found', resource: 'user', code: 'USER_NOT_FOUND'};
// Test error handlingit('should handle NotFoundError correctly', async () => { // Mock fetch to return 404 jest.spyOn(global, 'fetch').mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found', json: () => Promise.resolve(mockNotFoundResponse) } as Response);
try { await fetcher('/api/users/999', { errors: [NotFoundError] }); fail('Should have thrown NotFoundError'); } catch (error) { expect(error).toBeInstanceOf(NotFoundError); expect(error.data.resource).toBe('user'); }});Next Steps
- TypeScript Integration - Advanced TypeScript patterns
- React Query Integration - Use with React Query
- API Reference - Complete defineError documentation