Advanced Error Handling
Production-grade error handling strategies for resilient frontend applications
Advanced Error Handling
Error handling is not just about catching exceptions—it's about building resilient systems that gracefully handle failures, provide great user experience, and give developers actionable insights.
Why Advanced Error Handling?
Basic try-catch is not enough for production applications:
- User Errors vs Bugs: Different handling for expected vs unexpected errors
- Recovery Strategies: Retry, fallback, or degrade gracefully
- Context Preservation: Know what the user was doing when error occurred
- Cascading Failures: One error shouldn't break the entire app
- Actionable Reporting: Errors should help you fix issues quickly
Error Classification
1. User Errors (Expected)
Errors caused by user input or actions:
- Form validation failures
- Invalid search queries
- Network timeouts
- Authorization failures
Handling: Show friendly messages, guide user to fix
2. Application Errors (Bugs)
Unexpected errors in your code:
- Null reference errors
- Type errors
- Logic errors
- Integration failures
Handling: Log to monitoring, show fallback UI, alert team
3. System Errors (Infrastructure)
Platform or infrastructure issues:
- API downtime
- CDN failures
- Browser incompatibilities
- Resource exhaustion
Handling: Graceful degradation, retry with backoff, circuit breaker
Core Principles
1. Fail Gracefully
// ❌ BAD: Let errors crash the app
function ProductList() {
const products = fetchProducts(); // Throws on failure
return <div>{products.map(...)}</div>;
}
// ✅ GOOD: Handle errors gracefully
function ProductList() {
const { data, error, isLoading } = useProducts();
if (isLoading) return <Skeleton />;
if (error) return <ErrorFallback error={error} retry={refetch} />;
if (!data) return <EmptyState />;
return <div>{data.map(...)}</div>;
}2. Isolate Failures
// ❌ BAD: One error breaks everything
function Dashboard() {
return (
<>
<UserProfile />
<Notifications />
<ActivityFeed />
</>
);
}
// ✅ GOOD: Isolate with error boundaries
function Dashboard() {
return (
<>
<ErrorBoundary fallback={<ProfileError />}>
<UserProfile />
</ErrorBoundary>
<ErrorBoundary fallback={<NotificationsError />}>
<Notifications />
</ErrorBoundary>
<ErrorBoundary fallback={<FeedError />}>
<ActivityFeed />
</ErrorBoundary>
</>
);
}3. Provide Context
// ❌ BAD: Generic error with no context
throw new Error('Failed to load');
// ✅ GOOD: Rich error context
class LoadError extends Error {
constructor(
message: string,
public context: {
userId: string;
resource: string;
attemptNumber: number;
timestamp: Date;
}
) {
super(message);
this.name = 'LoadError';
}
}
throw new LoadError('Failed to load user profile', {
userId: user.id,
resource: '/api/profile',
attemptNumber: 3,
timestamp: new Date(),
});4. Enable Recovery
// Provide ways for users to recover
function ErrorFallback({ error, resetError }: ErrorFallbackProps) {
return (
<div className="error-container">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<div className="actions">
<button onClick={resetError}>Try Again</button>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
<button onClick={() => router.push('/')}>
Go Home
</button>
</div>
</div>
);
}Error Handling Strategy
// lib/error-handler.ts
export class ErrorHandler {
static handle(error: Error, context?: any) {
// 1. Classify error
const errorType = this.classify(error);
// 2. Log with context
this.log(error, errorType, context);
// 3. Report if needed
if (errorType === 'bug' || errorType === 'system') {
this.report(error, context);
}
// 4. Determine recovery strategy
return this.getRecoveryStrategy(errorType);
}
private static classify(error: Error): 'user' | 'bug' | 'system' {
// User errors
if (error instanceof ValidationError) return 'user';
if (error instanceof NotFoundError) return 'user';
// System errors
if (error instanceof NetworkError) return 'system';
if (error instanceof TimeoutError) return 'system';
// Everything else is a bug
return 'bug';
}
private static getRecoveryStrategy(type: string) {
const strategies = {
user: { retry: false, notify: true, fallback: false },
bug: { retry: false, notify: true, fallback: true },
system: { retry: true, notify: true, fallback: true },
};
return strategies[type];
}
}Error Types Hierarchy
// lib/errors.ts
// Base application error
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// User-facing errors (operational)
export class ValidationError extends AppError {
constructor(message: string, public field?: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 'NOT_FOUND', 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 'UNAUTHORIZED', 401);
}
}
// System errors (operational)
export class NetworkError extends AppError {
constructor(message: string, public originalError?: Error) {
super(message, 'NETWORK_ERROR', 503);
}
}
export class TimeoutError extends AppError {
constructor(operation: string, timeout: number) {
super(`${operation} timed out after ${timeout}ms`, 'TIMEOUT', 408);
}
}
// Programming errors (non-operational)
export class ConfigurationError extends AppError {
constructor(message: string) {
super(message, 'CONFIG_ERROR', 500, false);
}
}Monitoring Integration
// lib/error-reporter.ts
import * as Sentry from '@sentry/nextjs';
export function reportError(
error: Error,
context?: {
user?: any;
tags?: Record<string, string>;
level?: 'error' | 'warning' | 'info';
}
) {
// Don't report user errors to monitoring
if (error instanceof ValidationError) return;
if (error instanceof NotFoundError) return;
// Add context
Sentry.setContext('error_context', context);
// Set severity
Sentry.captureException(error, {
level: context?.level || 'error',
tags: context?.tags,
});
// Log locally for debugging
if (process.env.NODE_ENV === 'development') {
console.error('Error reported:', error, context);
}
}Best Practices
- Separate User Errors from Bugs: Handle them differently
- Add Context: Include user ID, action, timestamp
- Fail Gracefully: Never show blank screens
- Isolate Failures: Use error boundaries to contain errors
- Enable Recovery: Provide retry/reset options
- Monitor Proactively: Alert on error rate spikes
- Test Error States: Include error scenarios in tests
Common Pitfalls
❌ Catching all errors silently: Users see broken UI
✅ Show appropriate error UI
❌ Reporting every error to monitoring: Alert fatigue
✅ Report only bugs and system errors
❌ Generic error messages: "Something went wrong"
✅ Specific, actionable messages
❌ No error recovery: Users have to refresh
✅ Provide retry/reset mechanisms
Implementation Checklist
- Define error type hierarchy
- Set up error boundaries at key levels
- Implement error classification logic
- Configure error monitoring (Sentry, etc.)
- Add context to all error reports
- Implement retry strategies
- Design fallback UIs
- Add circuit breakers for external services
- Test error scenarios
- Document error handling patterns
Next Steps
Explore specific error handling patterns:
- Error Boundary Strategies: Granular isolation
- Error Tracking Context: Rich debugging info
- Error Recovery Patterns: Retry strategies
- Graceful Degradation: Fallback strategies
- Circuit Breaker: Prevent cascading failures
- Error Reporting Batch: Efficient error reporting