Error Handling
itty-fetcher provides robust error handling that's far superior to native fetch, with multiple approaches to suit different coding styles.
Automatic Error Throwing
Unlike native fetch (which only rejects on network errors), itty-fetcher throws on HTTP error status codes by default:
ts
try {
const data = await api.get('/nonexistent')
} catch (error) {
console.log(error.status) // 404
console.log(error.message) // "Not Found"
console.log(error.response) // Original Response object
}
Error Object Properties
Errors thrown by itty-fetcher include:
ts
interface FetcherError extends Error {
status: number // HTTP status code (404, 500, etc.)
message: string // Status text ("Not Found", "Internal Server Error")
response: Response // Original Response object
// ... plus any JSON error body properties
}
JSON Error Bodies
When the server returns JSON error responses, those properties are merged into the error:
ts
// Server returns: { error: "Validation failed", field: "email" }
try {
await api.post('/users', { name: 'Alice' })
} catch (error) {
console.log(error.status) // 400
console.log(error.error) // "Validation failed"
console.log(error.field) // "email"
}
Error Handling Approaches
1. Traditional Try/Catch
The standard approach for handling errors:
ts
async function fetchUser(id: string) {
try {
const user = await api.get(`/users/${id}`)
return user
} catch (error) {
if (error.status === 404) {
console.log('User not found')
return null
}
if (error.status >= 500) {
console.error('Server error:', error.message)
throw error // Re-throw server errors
}
console.error('Request failed:', error.status, error.message)
throw error
}
}
2. Promise Catch
Chain error handling directly on promises:
ts
const user = await api.get('/users/123')
.catch(error => {
if (error.status === 404) return null
throw error
})
const result = await api.get('/maybe-missing')
.catch(error => ({ error: error.status }))
if (result.error) {
console.log('Request failed with status:', result.error)
}
3. Tuple Mode (Go-style)
For error handling without try/catch, use tuple mode:
ts
const api = fetcher({ array: true })
const [error, users] = await api.get('/users')
if (error) {
console.log('Failed to fetch users:', error.status)
return
}
console.log('Users fetched successfully:', users)
Tuple Mode Examples
ts
const api = fetcher({ array: true })
// Handle different error types
const [error, user] = await api.get('/users/123')
if (error) {
switch (error.status) {
case 404:
console.log('User not found')
break
case 403:
console.log('Access denied')
break
default:
console.error('Unexpected error:', error.message)
}
return
}
// Successful response
console.log('User:', user)
Error Response Patterns
Handling Different Response Types
ts
// JSON error responses (most common)
try {
await api.post('/users', invalidData)
} catch (error) {
// error includes both status info AND JSON response data
console.log(error.status) // 400
console.log(error.message) // "Bad Request"
console.log(error.errors) // ["Email is required", "Name too short"]
}
// Text error responses
try {
await api.get('/text-endpoint')
} catch (error) {
console.log(error.status) // 500
console.log(error.message) // "Internal Server Error"
console.log(error.response) // Response object with .text() method
}
// Empty error responses
try {
await api.delete('/users/999')
} catch (error) {
console.log(error.status) // 404
console.log(error.message) // ""
console.log(error.response) // Response object
}
Graceful Degradation
ts
async function fetchUserWithFallback(id: string) {
return await api.get(`/users/${id}`)
.catch(error => {
if (error.status === 404) {
// Return default user for 404s
return { id, name: 'Unknown User', email: null }
}
if (error.status >= 500) {
// For server errors, return cached data if available
return getCachedUser(id) || null
}
// For other errors, re-throw
throw error
})
}
Global Error Handling
Using Response Interceptors
Handle errors globally using the after
option:
ts
const api = fetcher({
after: [
async (response) => {
// Log all successful responses
console.log('API Response:', response)
return response
}
]
})
// Note: `after` handlers only run on successful responses
// Errors are thrown before `after` handlers run
Wrapper Function Approach
Create a wrapper for consistent error handling:
ts
function createAPIWrapper(baseApi: Fetcher) {
const wrapper = {
async get(url: string, options?: any) {
try {
return await baseApi.get(url, options)
} catch (error) {
return handleError(error)
}
},
async post(url: string, data?: any, options?: any) {
try {
return await baseApi.post(url, data, options)
} catch (error) {
return handleError(error)
}
}
// ... other methods
}
function handleError(error: any) {
// Global error logging
console.error('API Error:', {
status: error.status,
message: error.message,
url: error.response?.url
})
// Transform or re-throw as needed
if (error.status === 401) {
redirectToLogin()
return null
}
throw error
}
return wrapper
}
const api = createAPIWrapper(fetcher('https://api.example.com'))
Testing Error Scenarios
ts
// Mock error responses for testing
const mockFetch = (status: number, body?: any) => {
return Promise.resolve(new Response(
body ? JSON.stringify(body) : null,
{ status, statusText: status === 404 ? 'Not Found' : 'Error' }
))
}
const api = fetcher({ fetch: mockFetch(404) })
try {
await api.get('/test')
} catch (error) {
expect(error.status).toBe(404)
expect(error.message).toBe('Not Found')
}
Best Practices
- Be Specific: Handle different error status codes differently
- Fail Fast: Don't catch errors you can't handle meaningfully
- Log Appropriately: Include relevant context in error logs
- User Experience: Provide helpful error messages to users
- Retries: For transient errors, consider implementing retry logic outside of itty-fetcher
- Monitoring: Track error rates and patterns in production
ts
// Good error handling example
async function saveUser(userData: UserData) {
try {
return await api.post('/users', userData)
} catch (error) {
// Specific handling for validation errors
if (error.status === 400) {
throw new ValidationError(error.errors || ['Invalid user data'])
}
// Authentication/authorization
if (error.status === 401) {
throw new AuthError('Authentication required')
}
if (error.status === 403) {
throw new AuthError('Permission denied')
}
// Server errors - log and provide generic message
if (error.status >= 500) {
console.error('Server error saving user:', error)
throw new Error('Unable to save user. Please try again.')
}
// Unexpected errors
console.error('Unexpected error:', error)
throw error
}
}