Back to Blog
Error HandlingJavaScriptBest PracticesDebugging

Error Handling Patterns for Robust Applications

Handle errors gracefully. From try-catch patterns to error boundaries to centralized error handling strategies.

B
Bootspring Team
Engineering
June 28, 2024
6 min read

Errors are inevitable. How you handle them determines whether your application gracefully recovers or crashes unexpectedly. Here are patterns for robust error handling.

The Problem with Silent Failures#

1// ❌ Silent failures hide bugs 2function getUser(id) { 3 try { 4 return database.findUser(id); 5 } catch (error) { 6 return null; // What went wrong? 7 } 8} 9 10// ❌ Unhandled promise rejections 11async function loadData() { 12 const data = await fetch('/api/data'); // No error handling 13 return data.json(); 14}

Custom Error Classes#

1// Base application error 2class AppError extends Error { 3 constructor( 4 message: string, 5 public code: string, 6 public statusCode: number = 500, 7 public isOperational: boolean = true 8 ) { 9 super(message); 10 this.name = this.constructor.name; 11 Error.captureStackTrace(this, this.constructor); 12 } 13} 14 15// Specific error types 16class ValidationError extends AppError { 17 constructor(message: string, public fields: Record<string, string>) { 18 super(message, 'VALIDATION_ERROR', 400); 19 } 20} 21 22class NotFoundError extends AppError { 23 constructor(resource: string, id: string) { 24 super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404); 25 } 26} 27 28class AuthenticationError extends AppError { 29 constructor(message = 'Authentication required') { 30 super(message, 'AUTHENTICATION_ERROR', 401); 31 } 32} 33 34class AuthorizationError extends AppError { 35 constructor(message = 'Permission denied') { 36 super(message, 'AUTHORIZATION_ERROR', 403); 37 } 38} 39 40// Usage 41throw new ValidationError('Invalid input', { 42 email: 'Email is required', 43 password: 'Password must be at least 8 characters', 44});

Result Pattern (No Exceptions)#

1// Result type 2type Result<T, E = Error> = 3 | { success: true; data: T } 4 | { success: false; error: E }; 5 6// Helper functions 7function ok<T>(data: T): Result<T, never> { 8 return { success: true, data }; 9} 10 11function err<E>(error: E): Result<never, E> { 12 return { success: false, error }; 13} 14 15// Usage 16async function findUser(id: string): Promise<Result<User, AppError>> { 17 try { 18 const user = await database.users.findUnique({ where: { id } }); 19 if (!user) { 20 return err(new NotFoundError('User', id)); 21 } 22 return ok(user); 23 } catch (error) { 24 return err(new AppError('Database error', 'DB_ERROR')); 25 } 26} 27 28// Handling results 29const result = await findUser('123'); 30 31if (result.success) { 32 console.log('Found user:', result.data.name); 33} else { 34 console.error('Error:', result.error.message); 35} 36 37// Chaining results 38async function getUserPosts(userId: string): Promise<Result<Post[], AppError>> { 39 const userResult = await findUser(userId); 40 if (!userResult.success) { 41 return userResult; // Propagate error 42 } 43 44 const posts = await database.posts.findMany({ 45 where: { authorId: userResult.data.id }, 46 }); 47 return ok(posts); 48}

Express Error Handling#

1// Async handler wrapper 2const asyncHandler = (fn: RequestHandler): RequestHandler => { 3 return (req, res, next) => { 4 Promise.resolve(fn(req, res, next)).catch(next); 5 }; 6}; 7 8// Route with error handling 9app.get('/users/:id', asyncHandler(async (req, res) => { 10 const user = await userService.findById(req.params.id); 11 if (!user) { 12 throw new NotFoundError('User', req.params.id); 13 } 14 res.json(user); 15})); 16 17// Global error handler 18app.use((error: Error, req: Request, res: Response, next: NextFunction) => { 19 // Log error 20 logger.error({ 21 error: error.message, 22 stack: error.stack, 23 path: req.path, 24 method: req.method, 25 }); 26 27 // Handle known errors 28 if (error instanceof AppError) { 29 return res.status(error.statusCode).json({ 30 error: { 31 code: error.code, 32 message: error.message, 33 ...(error instanceof ValidationError && { fields: error.fields }), 34 }, 35 }); 36 } 37 38 // Unknown errors 39 res.status(500).json({ 40 error: { 41 code: 'INTERNAL_ERROR', 42 message: 'An unexpected error occurred', 43 }, 44 }); 45});

React Error Boundaries#

1// Error boundary component 2class ErrorBoundary extends React.Component< 3 { fallback: React.ReactNode; children: React.ReactNode }, 4 { hasError: boolean; error: Error | null } 5> { 6 state = { hasError: false, error: null }; 7 8 static getDerivedStateFromError(error: Error) { 9 return { hasError: true, error }; 10 } 11 12 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 13 // Log to error reporting service 14 errorReporter.captureException(error, { 15 componentStack: errorInfo.componentStack, 16 }); 17 } 18 19 render() { 20 if (this.state.hasError) { 21 return this.props.fallback; 22 } 23 return this.props.children; 24 } 25} 26 27// Usage 28function App() { 29 return ( 30 <ErrorBoundary fallback={<ErrorPage />}> 31 <Router> 32 <Routes /> 33 </Router> 34 </ErrorBoundary> 35 ); 36} 37 38// Granular error boundaries 39function Dashboard() { 40 return ( 41 <div> 42 <ErrorBoundary fallback={<WidgetError />}> 43 <AnalyticsWidget /> 44 </ErrorBoundary> 45 46 <ErrorBoundary fallback={<WidgetError />}> 47 <RevenueWidget /> 48 </ErrorBoundary> 49 </div> 50 ); 51}

Retry Pattern#

1interface RetryOptions { 2 maxRetries: number; 3 delay: number; 4 backoff: 'linear' | 'exponential'; 5 retryCondition?: (error: Error) => boolean; 6} 7 8async function withRetry<T>( 9 fn: () => Promise<T>, 10 options: RetryOptions 11): Promise<T> { 12 const { maxRetries, delay, backoff, retryCondition } = options; 13 14 let lastError: Error; 15 16 for (let attempt = 0; attempt <= maxRetries; attempt++) { 17 try { 18 return await fn(); 19 } catch (error) { 20 lastError = error as Error; 21 22 // Check if we should retry 23 if (retryCondition && !retryCondition(lastError)) { 24 throw lastError; 25 } 26 27 if (attempt === maxRetries) { 28 throw lastError; 29 } 30 31 // Calculate delay 32 const waitTime = backoff === 'exponential' 33 ? delay * Math.pow(2, attempt) 34 : delay * (attempt + 1); 35 36 await new Promise((resolve) => setTimeout(resolve, waitTime)); 37 } 38 } 39 40 throw lastError!; 41} 42 43// Usage 44const data = await withRetry( 45 () => fetch('/api/data').then((r) => r.json()), 46 { 47 maxRetries: 3, 48 delay: 1000, 49 backoff: 'exponential', 50 retryCondition: (error) => { 51 // Only retry on network errors 52 return error.name === 'NetworkError'; 53 }, 54 } 55);

Centralized Error Logging#

1// Error reporter 2class ErrorReporter { 3 private context: Record<string, any> = {}; 4 5 setContext(key: string, value: any) { 6 this.context[key] = value; 7 } 8 9 captureException(error: Error, extra?: Record<string, any>) { 10 const payload = { 11 name: error.name, 12 message: error.message, 13 stack: error.stack, 14 context: this.context, 15 extra, 16 timestamp: new Date().toISOString(), 17 environment: process.env.NODE_ENV, 18 }; 19 20 // Send to error tracking service 21 if (process.env.NODE_ENV === 'production') { 22 this.sendToService(payload); 23 } else { 24 console.error('Error captured:', payload); 25 } 26 } 27 28 private async sendToService(payload: any) { 29 // Send to Sentry, Bugsnag, etc. 30 await fetch('/api/errors', { 31 method: 'POST', 32 headers: { 'Content-Type': 'application/json' }, 33 body: JSON.stringify(payload), 34 }); 35 } 36} 37 38export const errorReporter = new ErrorReporter(); 39 40// Global handlers 41window.onerror = (message, source, line, column, error) => { 42 errorReporter.captureException(error || new Error(String(message))); 43}; 44 45window.onunhandledrejection = (event) => { 46 errorReporter.captureException( 47 event.reason instanceof Error ? event.reason : new Error(String(event.reason)) 48 ); 49};

Best Practices#

DO: ✓ Use specific error types ✓ Include context in error messages ✓ Log errors with stack traces ✓ Fail fast on programmer errors ✓ Recover gracefully from operational errors ✓ Use error boundaries in React ✓ Validate input at boundaries DON'T: ✗ Catch and ignore errors ✗ Throw generic Error objects ✗ Expose internal errors to users ✗ Retry without backoff ✗ Log sensitive data in errors ✗ Use exceptions for control flow

Conclusion#

Good error handling is invisible when things go right and invaluable when they go wrong. Design your error handling strategy early, use typed errors, and always log enough context to debug issues in production.

Errors should tell a story—make sure yours are readable.

Share this article

Help spread the word about Bootspring