UNPKG

ws-dottie

Version:

Your friendly TypeScript companion for Washington State transportation APIs - WSDOT and WSF data with smart caching and React Query integration

532 lines (436 loc) 13.7 kB
# Error Handling Reference This guide covers error handling patterns and recovery strategies for WS-Dottie applications. > **📚 Documentation Navigation**: [Documentation Index](../../INDEX.md) • [Getting Started](../../getting-started.md) • [API Guide](../api-guide.md) ## 🚨 Error Types ### ApiError WS-Dottie uses a single error type for all API-related issues. All errors (network errors, validation errors, API errors) are wrapped in an `ApiError` object: ```typescript interface ApiError { /** Error name for type identification */ readonly name: "ApiError"; /** Human-readable error message (preserves original error) */ readonly message: string; /** HTTP status code if available */ readonly status?: number; /** Additional error context */ readonly context: { /** API endpoint that was called */ endpoint: string; /** Full request URL */ url: string; /** Timestamp when the error occurred */ timestamp: Date; /** Request identifier for tracking (optional) */ requestId?: string; }; } ``` **Important**: All errors in WS-Dottie are wrapped as `ApiError`, including: - Network errors (connection timeouts, DNS failures, etc.) - Validation errors (Zod schema validation failures) - API errors (HTTP status errors, authentication failures, etc.) The original error message is preserved in the `message` field, and the `context` object provides additional information about where and when the error occurred. ## 🔄 Error Handling Patterns ### Try-Catch Pattern ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; import { isApiError } from 'ws-dottie'; async function fetchWithErrorHandling() { try { const data = await fetchVesselLocations({ fetchMode: 'native', validate: true }); return data; } catch (error) { if (isApiError(error)) { console.error('API Error:', error.message); console.error('Status:', error.status); console.error('Context:', error.context); } else if (error instanceof Error) { console.error('Unexpected Error:', error.message); } else { console.error('Unknown error', error); } throw error; } } ``` ### Error Boundaries (React) ```javascript import { Component } from 'react'; import { isApiError } from 'ws-dottie'; class TransportationErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('Transportation Error:', error, errorInfo); if (isApiError(error)) { // Log API-specific context logErrorToService(error, errorInfo, { endpoint: error.context?.endpoint, status: error.status, }); } else { logErrorToService(error, errorInfo); } } render() { if (this.state.hasError) { return ( <div className="error-fallback"> <h2>Something went wrong</h2> <p>We're having trouble loading transportation data.</p> <details> <summary>Error Details</summary> <pre>{this.state.error?.message}</pre> </details> <button onClick={() => this.setState({ hasError: false, error: null })}> Try Again </button> </div> ); } return this.props.children; } } // Usage function App() { return ( <TransportationErrorBoundary> <TransportationDashboard /> </TransportationErrorBoundary> ); } ``` ### Async/Await Pattern ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; import { isApiError } from 'ws-dottie'; async function fetchWithAsyncAwait() { let vessels; try { vessels = await fetchVesselLocations({ fetchMode: 'native', validate: true }); } catch (error) { if (isApiError(error)) { console.error('Failed to fetch vessels:', error.message); // Handle specific API error if (error.status === 401) { // Handle authentication error showAuthErrorDialog(); } else if (error.status >= 500) { // Handle server error showServerErrorDialog(); } } else { // Handle other errors showGenericErrorDialog(); } // Return empty array as fallback return []; } return vessels; } ``` ## 🛠️ Recovery Strategies ### Retry Logic ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; import { isApiError } from 'ws-dottie'; class RetryableFetch { constructor() { this.maxRetries = 3; this.baseDelay = 1000; // 1 second this.maxDelay = 10000; // 10 seconds this.backoffFactor = 2; } async fetchWithRetry(fetchFunction, params = {}) { let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const data = await fetchFunction({ ...params, fetchMode: 'native', validate: true }); return data; } catch (error) { lastError = error; // Don't retry on authentication errors if (isApiError(error) && error.status === 401) { throw error; } // Don't retry on client errors (4xx) if (isApiError(error) && error.status >= 400 && error.status < 500) { throw error; } // If this is last attempt, throw error if (attempt === this.maxRetries) { throw error; } // Calculate delay for next attempt const delay = Math.min( this.baseDelay * Math.pow(this.backoffFactor, attempt - 1), this.maxDelay ); console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`); // Wait before retrying await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } } // Usage const retryableFetch = new RetryableFetch(); const vessels = await retryableFetch.fetchWithRetry(fetchVesselLocations); ``` ### Fallback Data ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; async function fetchWithFallback(fetchFunction, fallbackData = [], params = {}) { try { const data = await fetchFunction({ ...params, fetchMode: 'native', validate: true }); return data; } catch (error) { console.error('Failed to fetch data:', error.message); // Return fallback data return fallbackData; } } // Usage const vessels = await fetchWithFallback(fetchVesselLocations, [ { VesselID: 1, VesselName: 'Fallback Vessel 1' }, { VesselID: 2, VesselName: 'Fallback Vessel 2' } ]); ``` ### Graceful Degradation ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; class GracefulDegradation { constructor() { this.isHealthy = true; this.lastHealthCheck = Date.now(); } async checkHealth() { try { // Simple health check await fetchVesselLocations({ fetchMode: 'native', validate: true }); this.isHealthy = true; this.lastHealthCheck = Date.now(); } catch (error) { console.error('Health check failed:', error.message); this.isHealthy = false; } } async fetchWithGracefulDegradation(fetchFunction, params = {}) { // Check if service is healthy if (this.isHealthy || Date.now() - this.lastHealthCheck > 60000) { // 1 minute try { return await fetchFunction({ ...params, fetchMode: 'native', validate: true }); } catch (error) { console.error('API call failed:', error.message); throw error; } } else { // Service is unhealthy, return cached or fallback data console.warn('Service unhealthy, using fallback data'); return this.getCachedOrFallbackData(); } } getCachedOrFallbackData() { // Implementation depends on your caching strategy // This is a placeholder for actual implementation return []; } } // Usage const degradation = new GracefulDegradation(); // Periodic health check setInterval(() => degradation.checkHealth(), 30000); // Every 30 seconds // Fetch with graceful degradation const vessels = await degradation.fetchWithGracefulDegradation(fetchVesselLocations); ``` ## 📊 Error Monitoring ### Error Logging ```javascript import { fetchDottie, isApiError } from 'ws-dottie'; class ErrorLogger { constructor() { this.errors = []; } log(error, context = {}) { const errorEntry = { timestamp: new Date().toISOString(), message: error.message, stack: error.stack, context, ...(isApiError(error) && { status: error.status, endpoint: error.context?.endpoint, url: error.context?.url }) }; this.errors.push(errorEntry); // Log to console console.error('Transportation Error:', errorEntry); // In production, send to error tracking service if (process.env.NODE_ENV === 'production') { this.sendToErrorService(errorEntry); } } sendToErrorService(errorEntry) { // Implementation depends on your error tracking service // Example with a generic HTTP service fetch(process.env.ERROR_TRACKING_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorEntry) }).catch(err => { console.error('Failed to send error to tracking service:', err); }); } getErrorSummary() { const errorCounts = this.errors.reduce((acc, error) => { const key = error.message || 'Unknown error'; acc[key] = (acc[key] || 0) + 1; return acc; }, {}); return { total: this.errors.length, byMessage: errorCounts, recent: this.errors.slice(-10) // Last 10 errors }; } } // Usage with error handling wrapper const errorLogger = new ErrorLogger(); async function withErrorHandling(fn, context = {}) { try { return await fn(); } catch (error) { errorLogger.log(error, context); throw error; } } // Usage import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; async function main() { await withErrorHandling(async () => { const vessels = await fetchVesselLocations({ fetchMode: 'native', validate: true }); console.log(`Found ${vessels.length} vessels`); }, { operation: 'fetchVessels' }); // Print error summary console.log('Error Summary:', errorLogger.getErrorSummary()); } ``` ### Error Metrics ```javascript import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; import { isApiError } from 'ws-dottie'; class ErrorMetrics { constructor() { this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, errorsByStatus: {}, errorsByEndpoint: {}, averageResponseTime: 0, lastReset: Date.now() }; } recordRequest(endpointName, startTime, endTime, success, error = null) { this.metrics.totalRequests++; if (success) { this.metrics.successfulRequests++; } else { this.metrics.failedRequests++; if (isApiError(error) && error.status) { const status = error.status.toString(); this.metrics.errorsByStatus[status] = (this.metrics.errorsByStatus[status] || 0) + 1; } if (isApiError(error) && error.context?.endpoint) { const endpointName = error.context.endpoint; this.metrics.errorsByEndpoint[endpointName] = (this.metrics.errorsByEndpoint[endpointName] || 0) + 1; } } const responseTime = endTime - startTime; this.metrics.averageResponseTime = ( (this.metrics.averageResponseTime * (this.metrics.totalRequests - 1) + responseTime) / this.metrics.totalRequests ); } getMetrics() { const successRate = this.metrics.totalRequests > 0 ? (this.metrics.successfulRequests / this.metrics.totalRequests * 100).toFixed(2) + '%' : '0%'; return { ...this.metrics, successRate, errorRate: (100 - parseFloat(successRate)).toFixed(2) + '%' }; } reset() { this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, errorsByStatus: {}, errorsByEndpoint: {}, averageResponseTime: 0, lastReset: Date.now() }; } } // Usage const errorMetrics = new ErrorMetrics(); // Wrap fetch function to record metrics async function fetchWithMetrics(fetchFunction, params = {}) { const startTime = Date.now(); const endpointName = 'fetchVesselLocations'; // or extract from function name try { const data = await fetchFunction({ ...params, fetchMode: 'native', validate: true }); errorMetrics.recordRequest(endpointName, startTime, Date.now(), true); return data; } catch (error) { errorMetrics.recordRequest(endpointName, startTime, Date.now(), false, error); throw error; } } // Usage const vessels = await fetchWithMetrics(fetchVesselLocations); console.log('Current metrics:', errorMetrics.getMetrics()); ``` ## 📚 Next Steps - **[TanStack Query Guide](./tanstack-query.md)** - TanStack Query integration and caching - **[Fetching Data Guide](./fetching-data.md)** - Basic fetch-dottie usage patterns - **[CLI Usage Guide](./cli-usage.md)** - Command-line interface and debugging