defineError()
defineError()
Creates custom error classes for type-safe API error handling. These error classes can be used with the fetcher() function to handle specific HTTP error responses.
Signature
function defineError<TSchema extends StandardSchemaV1>( statusCode: number, schema: TSchema, name?: string): ApiErrorStatic<TSchema>Parameters
statusCode: number
The HTTP status code this error class should handle.
Examples:
400- Bad Request401- Unauthorized403- Forbidden404- Not Found422- Unprocessable Entity500- Internal Server Error
schema: TSchema
A Standard Schema compatible validation schema that describes the structure of the error response body.
Supported schema libraries:
- Zod
- Valibot
- ArkType
- Effect Schema
name?: string
Optional custom name for the error class. If not provided, a generic name will be used.
Return Value
Returns an error class constructor that can be:
- Used in the
errorsarray offetcher()calls - Used with
instanceofchecks in error handling - Instantiated directly for testing
The returned class extends ApiError and includes:
class CustomError extends ApiError { static statusCode: number; // The HTTP status code static schema: TSchema; // The validation schema
statusCode: number; // Instance property: HTTP status code data: InferSchema<TSchema>; // Validated error response data response: Response; // Original fetch Response object message: string; // Error message (from response.statusText) name: string; // Error class name}Examples
Basic Error Definition
import { defineError } from '@shkumbinhsn/fetcher';import { z } from 'zod';
// Simple error with messageconst NotFoundError = defineError( 404, z.object({ message: z.string(), resource: z.string() }), 'NotFoundError');
// Validation error with field detailsconst ValidationError = defineError( 400, z.object({ message: z.string(), errors: z.array(z.object({ field: z.string(), message: z.string(), code: z.string() })) }), 'ValidationError');Using with fetcher()
try { const user = await fetcher('/api/users/999', { schema: UserSchema, errors: [NotFoundError, ValidationError] });} catch (error) { if (error instanceof NotFoundError) { // error.data is typed as { message: string; resource: string } console.log(`${error.data.resource} not found: ${error.data.message}`); console.log(`Status: ${error.statusCode}`); // 404 } else if (error instanceof ValidationError) { // error.data is typed according to the schema console.log('Validation failed:'); error.data.errors.forEach(err => { console.log(` ${err.field}: ${err.message} (${err.code})`); }); }}Common Error Patterns
// Authentication errorsconst UnauthorizedError = defineError( 401, z.object({ message: z.string(), code: z.enum(['INVALID_TOKEN', 'TOKEN_EXPIRED', 'NO_TOKEN']) }), 'UnauthorizedError');
// Permission errorsconst ForbiddenError = defineError( 403, z.object({ message: z.string(), requiredPermissions: z.array(z.string()), userPermissions: z.array(z.string()) }), 'ForbiddenError');
// Rate limiting errorsconst RateLimitError = defineError( 429, z.object({ message: z.string(), retryAfter: z.number(), limit: z.number(), remaining: z.number(), resetTime: z.string() }), 'RateLimitError');
// Server errorsconst ServerError = defineError( 500, z.object({ message: z.string(), errorId: z.string(), timestamp: z.string(), details: z.record(z.any()).optional() }), 'ServerError');Complex Error Schemas
// GraphQL-style errorsconst 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.object({ code: z.string(), exception: z.record(z.any()).optional() }).optional() })), data: z.null().optional() }), 'GraphQLError');
// Conflict errors with detailed informationconst ConflictError = defineError( 409, z.object({ message: z.string(), conflictType: z.enum(['DUPLICATE_KEY', 'VERSION_MISMATCH', 'RESOURCE_LOCKED']), conflictingFields: z.array(z.string()), suggestions: z.array(z.string()).optional(), currentValue: z.any().optional(), attemptedValue: z.any().optional() }), 'ConflictError');Using with Different Schema Libraries
With Valibot
import * as v from 'valibot';
const ValidationError = defineError( 400, v.object({ message: v.string(), errors: v.array(v.object({ field: v.string(), message: v.string() })) }), 'ValidationError');With ArkType
import { type } from 'arktype';
const NotFoundError = defineError( 404, type({ message: 'string', resource: 'string', 'code?': 'string' }), 'NotFoundError');Error Class Properties and Methods
Static Properties
const MyError = defineError(404, schema, 'MyError');
console.log(MyError.statusCode); // 404console.log(MyError.schema); // The validation schemaInstance Properties
try { await fetcher('/api/endpoint', { errors: [MyError] });} catch (error) { if (error instanceof MyError) { console.log(error.statusCode); // HTTP status code console.log(error.data); // Validated error response data console.log(error.response); // Original Response object console.log(error.message); // Error message from response console.log(error.name); // Error class name }}Direct Instantiation
For testing or custom error creation:
const errorResponse = new Response( JSON.stringify({ message: 'Not found', resource: 'user' }), { status: 404, statusText: 'Not Found' });
const error = new NotFoundError( 'Not Found', { message: 'Not found', resource: 'user' }, errorResponse);
console.log(error instanceof NotFoundError); // trueconsole.log(error.data.resource); // 'user'Multiple Status Codes
To handle the same error format for multiple status codes, create separate error classes:
const BadRequestValidation = defineError(400, ValidationSchema, 'BadRequestValidation');const UnprocessableValidation = defineError(422, ValidationSchema, 'UnprocessableValidation');
// Use both in fetchertry { await fetcher('/api/endpoint', { errors: [BadRequestValidation, UnprocessableValidation] });} catch (error) { if (error instanceof BadRequestValidation || error instanceof UnprocessableValidation) { // Handle validation errors from either status code handleValidationError(error.data); }}
// Or create a type union for easier checkingtype ValidationError = | InstanceType<typeof BadRequestValidation> | InstanceType<typeof UnprocessableValidation>;
function isValidationError(error: unknown): error is ValidationError { return error instanceof BadRequestValidation || error instanceof UnprocessableValidation;}Error Inheritance
Error classes extend the base ApiError class:
import { ApiError } from '@shkumbinhsn/fetcher';
// All custom errors are instances of ApiErrortry { await fetcher('/api/endpoint', { errors: [NotFoundError, ValidationError] });} catch (error) { if (error instanceof ApiError) { // This catches any custom API error console.log('API error occurred:', error.statusCode); }
// More specific handling if (error instanceof NotFoundError) { console.log('Specific not found handling'); }}Best Practices
- Use descriptive names: Choose clear, specific names for your error classes
- Match your API: Create error schemas that match your actual API responses
- Handle common errors: Define errors for common HTTP status codes (400, 401, 403, 404, 500)
- Group related errors: Create error classes for each distinct error format your API returns
- Include helpful data: Design error schemas to include data useful for error handling and user messages
Testing
Test your error classes and handling:
// Mock fetch to return an error responseglobal.fetch = jest.fn().mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', json: () => Promise.resolve({ message: 'User not found', resource: 'user' })});
// Test error handlingit('should handle NotFoundError correctly', async () => { try { await fetcher('/api/users/999', { errors: [NotFoundError] }); fail('Should have thrown'); } catch (error) { expect(error).toBeInstanceOf(NotFoundError); expect(error.statusCode).toBe(404); expect(error.data.resource).toBe('user'); }});Related
fetcher()- Main fetcher function that uses error classesApiError- Base error class- Error Handling Guide - Comprehensive error handling guide