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.