Error handling separates amateur code from production-ready applications. Done poorly, errors crash systems and frustrate users. Done well, errors are caught, logged, reported, and recovered from gracefully. AI can help you implement robust error handling patterns.
The Error Handling Mindset#
Good error handling requires thinking about failure modes:
- What can go wrong? Network failures, invalid input, resource exhaustion
- How will we know? Logging, monitoring, alerting
- How will we respond? Retry, fallback, fail gracefully
- How will users be informed? Clear, actionable messages
Custom Error Classes#
Structured Errors#
1class AppError extends Error {
2 constructor(
3 message: string,
4 public readonly code: string,
5 public readonly statusCode: number = 500,
6 public readonly isOperational: boolean = true,
7 public readonly context?: Record<string, unknown>
8 ) {
9 super(message);
10 this.name = this.constructor.name;
11 Error.captureStackTrace(this, this.constructor);
12 }
13}
14
15class ValidationError extends AppError {
16 constructor(message: string, public readonly fields: Record<string, string>) {
17 super(message, 'VALIDATION_ERROR', 400, true, { fields });
18 }
19}
20
21class NotFoundError extends AppError {
22 constructor(resource: string, id: string) {
23 super(`${resource} not found: ${id}`, 'NOT_FOUND', 404, true, { resource, id });
24 }
25}
26
27class DatabaseError extends AppError {
28 constructor(message: string, public readonly originalError: Error) {
29 super(message, 'DATABASE_ERROR', 500, false, {
30 originalMessage: originalError.message
31 });
32 }
33}Error Factory Pattern#
1const Errors = {
2 validation: (fields: Record<string, string>) =>
3 new ValidationError('Validation failed', fields),
4
5 notFound: (resource: string, id: string) =>
6 new NotFoundError(resource, id),
7
8 unauthorized: (reason: string) =>
9 new AppError(reason, 'UNAUTHORIZED', 401, true),
10
11 forbidden: (action: string) =>
12 new AppError(`Not allowed: ${action}`, 'FORBIDDEN', 403, true),
13
14 rateLimit: (limit: number, window: string) =>
15 new AppError(`Rate limit exceeded: ${limit} per ${window}`, 'RATE_LIMIT', 429, true),
16
17 internal: (message: string, context?: Record<string, unknown>) =>
18 new AppError(message, 'INTERNAL_ERROR', 500, false, context)
19};
20
21// Usage
22throw Errors.notFound('User', userId);
23throw Errors.validation({ email: 'Invalid email format' });Error Handling Patterns#
Try-Catch Best Practices#
1// ❌ Catching everything silently
2try {
3 await riskyOperation();
4} catch (e) {
5 // Silently swallowed
6}
7
8// ❌ Generic catch with no handling
9try {
10 await riskyOperation();
11} catch (e) {
12 console.log(e);
13}
14
15// ✅ Specific, informative handling
16try {
17 await riskyOperation();
18} catch (error) {
19 if (error instanceof NetworkError) {
20 logger.warn('Network error, retrying...', { error });
21 return retry(riskyOperation);
22 }
23
24 if (error instanceof ValidationError) {
25 // User error, return appropriate response
26 throw error;
27 }
28
29 // Unexpected error, log and re-throw
30 logger.error('Unexpected error in riskyOperation', {
31 error,
32 context: { input }
33 });
34 throw error;
35}Result Pattern (No Exceptions)#
1type Result<T, E = Error> =
2 | { success: true; data: T }
3 | { success: false; error: E };
4
5async function parseUserInput(input: string): Promise<Result<User, ValidationError>> {
6 try {
7 const validated = schema.parse(input);
8 return { success: true, data: validated };
9 } catch (error) {
10 if (error instanceof z.ZodError) {
11 return {
12 success: false,
13 error: new ValidationError('Invalid input', formatZodErrors(error))
14 };
15 }
16 throw error; // Unexpected error, let it bubble
17 }
18}
19
20// Usage
21const result = await parseUserInput(input);
22if (!result.success) {
23 return res.status(400).json({ errors: result.error.fields });
24}
25const user = result.data;Error Boundaries (React)#
1class ErrorBoundary extends Component<Props, State> {
2 state = { hasError: false, error: null };
3
4 static getDerivedStateFromError(error: Error) {
5 return { hasError: true, error };
6 }
7
8 componentDidCatch(error: Error, errorInfo: ErrorInfo) {
9 logger.error('React error boundary caught error', {
10 error,
11 componentStack: errorInfo.componentStack
12 });
13
14 // Report to error tracking service
15 errorTracker.captureException(error, { extra: errorInfo });
16 }
17
18 render() {
19 if (this.state.hasError) {
20 return this.props.fallback || <ErrorFallback error={this.state.error} />;
21 }
22 return this.props.children;
23 }
24}
25
26// Usage
27<ErrorBoundary fallback={<PaymentErrorUI />}>
28 <PaymentForm />
29</ErrorBoundary>Global Error Handling#
Express Error Handler#
1// Error handling middleware (must be last)
2app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
3 // Generate request ID for correlation
4 const requestId = req.headers['x-request-id'] || generateId();
5
6 // Log error with context
7 const logContext = {
8 requestId,
9 method: req.method,
10 path: req.path,
11 userId: req.user?.id,
12 error: {
13 name: error.name,
14 message: error.message,
15 stack: error.stack
16 }
17 };
18
19 if (error instanceof AppError) {
20 if (!error.isOperational) {
21 logger.error('Non-operational error', logContext);
22 } else {
23 logger.warn('Operational error', logContext);
24 }
25
26 return res.status(error.statusCode).json({
27 error: {
28 code: error.code,
29 message: error.message,
30 ...(error instanceof ValidationError && { fields: error.fields }),
31 requestId
32 }
33 });
34 }
35
36 // Unexpected error
37 logger.error('Unexpected error', logContext);
38
39 // Don't leak error details in production
40 res.status(500).json({
41 error: {
42 code: 'INTERNAL_ERROR',
43 message: process.env.NODE_ENV === 'production'
44 ? 'An unexpected error occurred'
45 : error.message,
46 requestId
47 }
48 });
49});Unhandled Rejection Handling#
1process.on('unhandledRejection', (reason, promise) => {
2 logger.error('Unhandled Rejection', {
3 reason,
4 promise
5 });
6
7 // In production, you might want to:
8 // 1. Report to error tracking
9 // 2. Gracefully shutdown
10 // 3. Let process manager restart
11});
12
13process.on('uncaughtException', (error) => {
14 logger.error('Uncaught Exception', { error });
15
16 // Must exit process after uncaught exception
17 process.exit(1);
18});Async Error Handling#
Promise Chain Errors#
1// ❌ Missing error handling
2fetchUser(userId)
3 .then(user => fetchOrders(user.id))
4 .then(orders => processOrders(orders));
5
6// ✅ Proper error handling
7fetchUser(userId)
8 .then(user => fetchOrders(user.id))
9 .then(orders => processOrders(orders))
10 .catch(error => {
11 if (error instanceof NotFoundError) {
12 return handleNotFound(error);
13 }
14 throw error;
15 });
16
17// ✅ Even better: async/await with try-catch
18async function getUserOrders(userId: string) {
19 try {
20 const user = await fetchUser(userId);
21 const orders = await fetchOrders(user.id);
22 return processOrders(orders);
23 } catch (error) {
24 if (error instanceof NotFoundError) {
25 return handleNotFound(error);
26 }
27 throw error;
28 }
29}Parallel Operations#
1// ❌ Promise.all fails fast, loses other results
2const [users, orders, products] = await Promise.all([
3 fetchUsers(),
4 fetchOrders(),
5 fetchProducts()
6]);
7
8// ✅ Promise.allSettled for independent operations
9const results = await Promise.allSettled([
10 fetchUsers(),
11 fetchOrders(),
12 fetchProducts()
13]);
14
15const data = {
16 users: results[0].status === 'fulfilled' ? results[0].value : [],
17 orders: results[1].status === 'fulfilled' ? results[1].value : [],
18 products: results[2].status === 'fulfilled' ? results[2].value : []
19};
20
21// Log any failures
22results.forEach((result, index) => {
23 if (result.status === 'rejected') {
24 logger.error(`Fetch ${['users', 'orders', 'products'][index]} failed`, {
25 error: result.reason
26 });
27 }
28});User-Facing Error Messages#
Error Message Guidelines#
1const userMessages = {
2 VALIDATION_ERROR: 'Please check your input and try again.',
3 NOT_FOUND: 'The requested item could not be found.',
4 UNAUTHORIZED: 'Please sign in to continue.',
5 FORBIDDEN: 'You don\'t have permission to do this.',
6 RATE_LIMIT: 'Too many requests. Please wait a moment.',
7 NETWORK_ERROR: 'Connection problem. Please check your internet.',
8 INTERNAL_ERROR: 'Something went wrong. Please try again.',
9};
10
11function getUserMessage(error: AppError): string {
12 return userMessages[error.code] || userMessages.INTERNAL_ERROR;
13}Actionable Error UI#
1function ErrorDisplay({ error, onRetry }: ErrorDisplayProps) {
2 return (
3 <div className="error-container">
4 <ErrorIcon />
5 <h3>{getUserMessage(error)}</h3>
6
7 {error instanceof ValidationError && (
8 <ul className="validation-errors">
9 {Object.entries(error.fields).map(([field, message]) => (
10 <li key={field}>{message}</li>
11 ))}
12 </ul>
13 )}
14
15 {error.code === 'NETWORK_ERROR' && (
16 <button onClick={onRetry}>Try Again</button>
17 )}
18
19 {error.code === 'UNAUTHORIZED' && (
20 <Link to="/login">Sign In</Link>
21 )}
22
23 <details>
24 <summary>Technical Details</summary>
25 <code>Error Code: {error.code}</code>
26 </details>
27 </div>
28 );
29}Error Monitoring and Alerting#
Error Tracking Integration#
1import * as Sentry from '@sentry/node';
2
3Sentry.init({
4 dsn: process.env.SENTRY_DSN,
5 environment: process.env.NODE_ENV,
6 beforeSend(event, hint) {
7 // Filter out operational errors
8 const error = hint.originalException as AppError;
9 if (error?.isOperational) {
10 return null; // Don't send to Sentry
11 }
12 return event;
13 }
14});
15
16// Add context to errors
17app.use((req, res, next) => {
18 Sentry.setUser({ id: req.user?.id, email: req.user?.email });
19 Sentry.setTag('request_id', req.headers['x-request-id']);
20 next();
21});Conclusion#
Error handling is about anticipating failure and responding gracefully. With custom error classes, structured handling, and proper monitoring, your application becomes resilient and debuggable.
AI helps implement these patterns correctly from the start—from custom error hierarchies to global handlers to user-facing messages. The result is an application that handles the unexpected without breaking, frustrating, or confusing users.