Error Handling
Graceful Failure and Recovery
Explanation
Why Error Handling Matters
Errors are inevitable. How you handle them determines user experience and debugging efficiency. Good error handling is informative, recoverable, and secure.
Key Principles
- Fail Fast: Detect errors early
- Fail Gracefully: Provide useful feedback
- Log Appropriately: Capture context for debugging
- Don't Expose Internals: Security through obscurity
Demonstration
Example 1: JavaScript Error Handling
// Custom error classes
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', 400);
this.errors = errors;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
// Async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage in routes
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json({ data: user });
}));
// Global error handler
app.use((err, req, res, next) => {
// Log error
console.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
timestamp: new Date().toISOString()
});
// Operational errors - send message to client
if (err.isOperational) {
return res.status(err.statusCode).json({
error: err.message,
...(err.errors && { details: err.errors })
});
}
// Programming errors - don't leak details
res.status(500).json({
error: 'Something went wrong'
});
});
// Unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// Optionally restart the process
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
process.exit(1);
});
Example 2: Try-Catch Patterns
// Basic try-catch
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
// Network error
console.error('Network error:', error);
throw new AppError('Network unavailable', 503);
}
throw error;
}
}
// Try-catch with cleanup
async function processFile(path) {
let file;
try {
file = await fs.open(path, 'r');
const content = await file.readFile('utf8');
return processContent(content);
} catch (error) {
if (error.code === 'ENOENT') {
throw new NotFoundError('File');
}
throw error;
} finally {
// Always runs
if (file) {
await file.close();
}
}
}
// Optional catch binding (ES2019)
try {
JSON.parse(invalidJson);
} catch {
// Don't need error variable
return defaultValue;
}
// Error boundaries in React
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to error reporting service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
Example 3: API Error Responses
// Standardized error response format
const errorResponse = {
error: {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: 'Must be at least 8 characters' }
]
},
meta: {
timestamp: '2024-01-15T10:30:00Z',
requestId: 'abc-123-def'
}
};
// Error codes enum
const ErrorCodes = {
VALIDATION_ERROR: 'VALIDATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
RATE_LIMITED: 'RATE_LIMITED',
INTERNAL_ERROR: 'INTERNAL_ERROR'
};
// Error factory
function createErrorResponse(code, message, details = null) {
return {
error: {
code,
message,
...(details && { details })
},
meta: {
timestamp: new Date().toISOString(),
requestId: req.id
}
};
}
// HTTP status code mapping
const statusCodeMap = {
[ErrorCodes.VALIDATION_ERROR]: 400,
[ErrorCodes.NOT_FOUND]: 404,
[ErrorCodes.UNAUTHORIZED]: 401,
[ErrorCodes.FORBIDDEN]: 403,
[ErrorCodes.RATE_LIMITED]: 429,
[ErrorCodes.INTERNAL_ERROR]: 500
};
Example 4: Logging Best Practices
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'api' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Structured logging
logger.info('User created', {
userId: user.id,
email: user.email,
action: 'create_user'
});
logger.error('Payment failed', {
userId: user.id,
orderId: order.id,
error: error.message,
stack: error.stack,
paymentMethod: order.paymentMethod
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request completed', {
method: req.method,
path: req.path,
status: res.statusCode,
duration,
userAgent: req.get('user-agent'),
ip: req.ip
});
});
next();
});
// Error logging with context
function logError(error, context = {}) {
logger.error(error.message, {
...context,
errorName: error.name,
errorCode: error.code,
stack: error.stack,
timestamp: new Date().toISOString()
});
}
Key Takeaways:
- Create custom error classes
- Use async error handlers
- Standardize error responses
- Log with context
- Never expose stack traces to users
Imitation
Challenge 1: Build Error Reporting
Task: Create an error reporting utility that sends errors to a service.
Solution
class ErrorReporter {
constructor(options = {}) {
this.endpoint = options.endpoint;
this.appName = options.appName || 'app';
this.environment = options.environment || 'development';
this.queue = [];
this.batchSize = options.batchSize || 10;
this.flushInterval = options.flushInterval || 5000;
setInterval(() => this.flush(), this.flushInterval);
}
capture(error, context = {}) {
const report = {
message: error.message,
name: error.name,
stack: error.stack,
timestamp: new Date().toISOString(),
app: this.appName,
environment: this.environment,
context: {
url: typeof window !== 'undefined' ? window.location.href : null,
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
...context
}
};
this.queue.push(report);
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ errors: batch })
});
} catch (e) {
// Re-queue on failure
this.queue.unshift(...batch);
}
}
// Global error handler
install() {
window.onerror = (message, source, line, column, error) => {
this.capture(error || new Error(message), {
source, line, column
});
};
window.onunhandledrejection = (event) => {
this.capture(event.reason);
};
}
}
const reporter = new ErrorReporter({
endpoint: '/api/errors',
appName: 'my-app'
});
reporter.install();
Practice
Exercise 1: Retry Logic
Difficulty: Intermediate
Implement a retry utility that:
- Retries failed operations
- Uses exponential backoff
- Has configurable max retries
Exercise 2: Circuit Breaker
Difficulty: Advanced
Build a circuit breaker pattern:
- Opens on repeated failures
- Closes after timeout
- Half-open state for testing
Summary
What you learned:
- Custom error classes
- Async error handling
- Standardized error responses
- Structured logging
- Error boundaries
Next Steps:
- Read: Monitoring
- Practice: Add error handling to your API
- Explore: Sentry, LogRocket
