Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand global error handling fundamentals ๐ฏ
- Apply error handling in real projects ๐๏ธ
- Debug common error handling issues ๐
- Write type-safe error handling code โจ
๐ฏ Introduction
Welcome to this essential tutorial on global error handling in TypeScript! ๐ In this guide, weโll explore how to catch and handle errors gracefully across your entire application.
Youโll discover how global error handlers can transform your backend development experience. Whether youโre building Express APIs ๐, microservices ๐ฅ๏ธ, or complex server applications ๐, understanding error handling is crucial for building robust, maintainable applications.
By the end of this tutorial, youโll feel confident implementing comprehensive error handling in your TypeScript projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Global Error Handling
๐ค What is Global Error Handling?
Global error handling is like having a safety net for your entire application ๐ธ๏ธ. Think of it as a friendly guardian that catches any errors that slip through your normal error handling, ensuring your app doesnโt crash unexpectedly.
In TypeScript terms, a global error handler is a centralized system that catches unhandled errors, processes them consistently, and responds appropriately ๐ก๏ธ. This means you can:
- โจ Prevent application crashes
- ๐ Log errors consistently
- ๐ก๏ธ Provide user-friendly error responses
- ๐ Monitor application health
๐ก Why Use Global Error Handling?
Hereโs why developers love global error handlers:
- Crash Prevention ๐: Keep your app running even when unexpected errors occur
- Consistent Logging ๐ป: All errors get logged in the same format
- Better User Experience ๐: Users see friendly messages instead of stack traces
- Debugging Made Easy ๐ง: Centralized error information helps you fix issues faster
Real-world example: Imagine building an e-commerce API ๐. With global error handling, a database connection error wonโt crash your entire server - instead, users get a friendly โPlease try again laterโ message while you get detailed logs to fix the issue.
๐ง Basic Syntax and Usage
๐ Simple Express Error Handler
Letโs start with a friendly example:
// ๐ Hello, Express error handling!
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// ๐จ Creating a simple error interface
interface AppError extends Error {
statusCode?: number; // ๐จ HTTP status code
isOperational?: boolean; // ๐ฏ Is this a known error?
}
// ๐ก๏ธ Global error handler middleware
const globalErrorHandler = (
err: AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
// ๐ Log the error details
console.error('๐จ Error caught:', err.message);
console.error('๐ Stack trace:', err.stack);
// ๐ฏ Send user-friendly response
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Something went wrong! ๐ฐ';
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
// ๐ง Register the error handler (must be last!)
app.use(globalErrorHandler);
๐ก Explanation: Notice how we use a custom AppError
interface and provide different responses for development vs production! The error handler must be registered last in Express.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Custom error classes
class ValidationError extends Error {
public readonly statusCode = 400;
public readonly isOperational = true;
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
class DatabaseError extends Error {
public readonly statusCode = 500;
public readonly isOperational = true;
constructor(message: string = 'Database operation failed ๐พ') {
super(message);
this.name = 'DatabaseError';
}
}
// ๐จ Pattern 2: Error factory
const createError = (message: string, statusCode: number = 500): AppError => {
const error = new Error(message) as AppError;
error.statusCode = statusCode;
error.isOperational = true;
return error;
};
// ๐ Pattern 3: Async error wrapper
const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
๐ก Practical Examples
๐ Example 1: E-commerce API Error Handler
Letโs build something real:
// ๐๏ธ Define our error types
interface APIError extends Error {
statusCode: number;
isOperational: boolean;
errorCode?: string; // ๐ท๏ธ Custom error codes
}
// ๐ E-commerce specific errors
class ProductNotFoundError extends Error implements APIError {
public readonly statusCode = 404;
public readonly isOperational = true;
public readonly errorCode = 'PRODUCT_NOT_FOUND';
constructor(productId: string) {
super(`Product with ID ${productId} not found ๐`);
this.name = 'ProductNotFoundError';
}
}
class InsufficientStockError extends Error implements APIError {
public readonly statusCode = 400;
public readonly isOperational = true;
public readonly errorCode = 'INSUFFICIENT_STOCK';
constructor(available: number, requested: number) {
super(`Only ${available} items available, but ${requested} requested ๐ฆ`);
this.name = 'InsufficientStockError';
}
}
// ๐ฏ Comprehensive error handler
class ErrorHandler {
// ๐จ Main error handling method
public static handleError(
err: APIError,
req: Request,
res: Response,
next: NextFunction
): void {
// ๐ Log error details
ErrorHandler.logError(err, req);
// ๐จ Format error response
const errorResponse = ErrorHandler.formatErrorResponse(err);
// ๐ค Send response
res.status(err.statusCode || 500).json(errorResponse);
}
// ๐ Log error with context
private static logError(err: APIError, req: Request): void {
const errorInfo = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
error: {
name: err.name,
message: err.message,
stack: err.stack,
statusCode: err.statusCode,
errorCode: err.errorCode
}
};
// ๐ Different log levels based on error severity
if (err.statusCode >= 500) {
console.error('๐จ CRITICAL ERROR:', JSON.stringify(errorInfo, null, 2));
} else {
console.warn('โ ๏ธ CLIENT ERROR:', JSON.stringify(errorInfo, null, 2));
}
}
// ๐จ Format consistent error responses
private static formatErrorResponse(err: APIError) {
const isProduction = process.env.NODE_ENV === 'production';
return {
success: false,
error: {
message: err.isOperational ? err.message : 'Internal server error ๐ฐ',
code: err.errorCode || 'UNKNOWN_ERROR',
statusCode: err.statusCode || 500,
timestamp: new Date().toISOString(),
// ๐ง Include stack trace only in development
...((!isProduction && err.stack) && { stack: err.stack })
}
};
}
}
// ๐ฎ Let's use it in routes!
app.get('/products/:id', asyncHandler(async (req: Request, res: Response) => {
const { id } = req.params;
// ๐ Simulate product lookup
const product = await findProductById(id);
if (!product) {
throw new ProductNotFoundError(id);
}
res.json({ success: true, data: product });
}));
app.post('/cart/add', asyncHandler(async (req: Request, res: Response) => {
const { productId, quantity } = req.body;
// ๐ฆ Check stock availability
const stock = await getProductStock(productId);
if (stock < quantity) {
throw new InsufficientStockError(stock, quantity);
}
// โ
Add to cart logic here
res.json({ success: true, message: 'Item added to cart! ๐' });
}));
// ๐ก๏ธ Register our error handler
app.use(ErrorHandler.handleError);
๐ฏ Try it yourself: Add a PaymentFailedError
class and handle credit card processing failures!
๐ฎ Example 2: Game Server Error Handling
Letโs make error handling fun:
// ๐ Game-specific error types
enum GameErrorCode {
PLAYER_NOT_FOUND = 'PLAYER_NOT_FOUND',
GAME_FULL = 'GAME_FULL',
INVALID_MOVE = 'INVALID_MOVE',
GAME_OVER = 'GAME_OVER',
CONNECTION_LOST = 'CONNECTION_LOST'
}
interface GameError extends Error {
code: GameErrorCode;
playerId?: string;
gameId?: string;
severity: 'low' | 'medium' | 'high' | 'critical';
}
class GameErrorHandler {
// ๐ฎ Handle game-specific errors
public static handleGameError(error: GameError): void {
// ๐ Log based on severity
switch (error.severity) {
case 'critical':
console.error('๐จ CRITICAL GAME ERROR:', {
code: error.code,
message: error.message,
playerId: error.playerId,
gameId: error.gameId,
timestamp: new Date().toISOString()
});
// ๐ง Could send alerts to development team
break;
case 'high':
console.error('๐ด HIGH PRIORITY:', error.message);
break;
case 'medium':
console.warn('๐ก MEDIUM PRIORITY:', error.message);
break;
case 'low':
console.info('๐ข LOW PRIORITY:', error.message);
break;
}
// ๐ฏ Take appropriate action
GameErrorHandler.handleErrorAction(error);
}
// ๐ช Different actions based on error type
private static handleErrorAction(error: GameError): void {
switch (error.code) {
case GameErrorCode.CONNECTION_LOST:
// ๐ Attempt reconnection
console.log('๐ Attempting to reconnect player...');
break;
case GameErrorCode.GAME_FULL:
// ๐ Add to waiting list
console.log('๐ Adding player to waiting list...');
break;
case GameErrorCode.INVALID_MOVE:
// โ ๏ธ Notify player
console.log('โ ๏ธ Notifying player of invalid move...');
break;
default:
console.log('๐ค Handling generic error...');
}
}
}
// ๐ฎ Example usage in game server
const processPlayerMove = (playerId: string, move: any): void => {
try {
// ๐ฏ Game logic here
validateMove(move);
applyMove(playerId, move);
} catch (error) {
const gameError: GameError = {
name: 'InvalidMoveError',
message: `Invalid move by player ${playerId} ๐ฎ`,
code: GameErrorCode.INVALID_MOVE,
playerId,
severity: 'medium'
};
GameErrorHandler.handleGameError(gameError);
}
};
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Error Recovery Strategies
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced error recovery system
interface RecoveryStrategy {
canRecover(error: Error): boolean;
recover(error: Error, context: any): Promise<any>;
fallback(error: Error, context: any): any;
}
class DatabaseRecoveryStrategy implements RecoveryStrategy {
private retryAttempts = 3;
private retryDelay = 1000; // ๐ 1 second
canRecover(error: Error): boolean {
// ๐ Check if it's a recoverable database error
return error.message.includes('connection') ||
error.message.includes('timeout');
}
async recover(error: Error, context: any): Promise<any> {
console.log('๐ Attempting database recovery...');
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
try {
// โณ Wait before retry
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
// ๐ Retry the operation
return await context.operation();
} catch (retryError) {
console.log(`๐ฅ Retry attempt ${attempt} failed`);
if (attempt === this.retryAttempts) {
throw retryError;
}
}
}
}
fallback(error: Error, context: any): any {
// ๐ Return cached data or default response
console.log('๐ Using fallback strategy...');
return { success: false, message: 'Service temporarily unavailable ๐ง' };
}
}
// ๐ช Error recovery manager
class ErrorRecoveryManager {
private strategies: RecoveryStrategy[] = [];
addStrategy(strategy: RecoveryStrategy): void {
this.strategies.push(strategy);
}
async handleErrorWithRecovery(error: Error, context: any): Promise<any> {
// ๐ Find a strategy that can handle this error
const strategy = this.strategies.find(s => s.canRecover(error));
if (strategy) {
try {
console.log('โจ Attempting error recovery...');
return await strategy.recover(error, context);
} catch (recoveryError) {
console.log('๐ซ Recovery failed, using fallback...');
return strategy.fallback(error, context);
}
}
// ๐ฅ No recovery strategy available
throw error;
}
}
๐๏ธ Advanced Topic 2: Error Monitoring Integration
For the brave developers:
// ๐ Integration with monitoring services
interface ErrorMonitor {
reportError(error: Error, context: ErrorContext): void;
reportMetric(metric: string, value: number): void;
}
interface ErrorContext {
userId?: string;
requestId?: string;
environment: string;
timestamp: Date;
metadata?: Record<string, any>;
}
class ProductionErrorMonitor implements ErrorMonitor {
reportError(error: Error, context: ErrorContext): void {
// ๐ Send to external monitoring service (like Sentry)
const errorReport = {
message: error.message,
stack: error.stack,
level: this.getErrorLevel(error),
user: { id: context.userId },
request: { id: context.requestId },
environment: context.environment,
timestamp: context.timestamp.toISOString(),
extra: context.metadata
};
// ๐ This would integrate with your monitoring service
console.log('๐ Reporting to monitoring service:', errorReport);
}
reportMetric(metric: string, value: number): void {
// ๐ Track error metrics
console.log(`๐ Metric: ${metric} = ${value}`);
}
private getErrorLevel(error: Error): string {
if (error.name.includes('Critical')) return 'error';
if (error.name.includes('Warning')) return 'warning';
return 'info';
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Swallowing Errors
// โ Wrong way - hiding errors!
const handleRequest = async (req: Request, res: Response) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (error) {
// ๐ฅ Silent failure - user never knows what happened!
res.json({ success: false });
}
};
// โ
Correct way - proper error handling!
const handleRequest = async (req: Request, res: Response, next: NextFunction) => {
try {
const data = await someAsyncOperation();
res.json({ success: true, data });
} catch (error) {
// ๐ฏ Pass to global error handler
next(error);
}
};
๐คฏ Pitfall 2: Exposing Sensitive Information
// โ Dangerous - exposing internal details!
const errorHandler = (err: Error, req: Request, res: Response) => {
res.status(500).json({
error: err.message,
stack: err.stack, // ๐ฅ Exposes internal structure!
query: req.query // ๐ฅ Might contain sensitive data!
});
};
// โ
Safe - sanitized error responses!
const errorHandler = (err: Error, req: Request, res: Response) => {
const isProduction = process.env.NODE_ENV === 'production';
res.status(500).json({
success: false,
message: isProduction ? 'Internal server error' : err.message,
// ๐ก๏ธ Only include debug info in development
...((!isProduction) && {
stack: err.stack,
timestamp: new Date().toISOString()
})
});
};
๐ ๏ธ Best Practices
- ๐ฏ Be Specific: Create custom error classes for different scenarios
- ๐ Log Everything: Include context like user ID, request ID, timestamp
- ๐ก๏ธ Sanitize Responses: Never expose sensitive data in error messages
- ๐จ Use Error Codes: Implement consistent error codes for client handling
- โจ Plan Recovery: Always have fallback strategies for critical operations
- ๐ Monitor Metrics: Track error rates and patterns
- ๐ Test Error Paths: Write tests for your error handling code
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Robust API Error System
Create a comprehensive error handling system for a social media API:
๐ Requirements:
- โ Custom error classes for different scenarios (UserNotFound, PostNotFound, etc.)
- ๐ท๏ธ Error codes and severity levels
- ๐ค Context tracking (user ID, request ID)
- ๐ Rate limiting error handling
- ๐จ Different responses for different client types (web vs mobile)
- ๐ Error metrics and monitoring
๐ Bonus Points:
- Add error recovery for database failures
- Implement circuit breaker pattern
- Create error notification system
- Add error correlation across microservices
๐ก Solution
๐ Click to see solution
// ๐ฏ Our comprehensive error handling system!
// ๐ Base error interface
interface APIError extends Error {
statusCode: number;
errorCode: string;
severity: 'low' | 'medium' | 'high' | 'critical';
isOperational: boolean;
context?: Record<string, any>;
}
// ๐๏ธ Custom error classes
class UserNotFoundError extends Error implements APIError {
public readonly statusCode = 404;
public readonly errorCode = 'USER_NOT_FOUND';
public readonly severity = 'medium' as const;
public readonly isOperational = true;
constructor(userId: string, context?: Record<string, any>) {
super(`User with ID ${userId} not found ๐ค`);
this.name = 'UserNotFoundError';
this.context = context;
}
}
class RateLimitExceededError extends Error implements APIError {
public readonly statusCode = 429;
public readonly errorCode = 'RATE_LIMIT_EXCEEDED';
public readonly severity = 'low' as const;
public readonly isOperational = true;
constructor(limit: number, resetTime: Date) {
super(`Rate limit exceeded. Limit: ${limit}/hour. Resets at: ${resetTime.toISOString()} โฐ`);
this.name = 'RateLimitExceededError';
}
}
class DatabaseConnectionError extends Error implements APIError {
public readonly statusCode = 503;
public readonly errorCode = 'DATABASE_UNAVAILABLE';
public readonly severity = 'critical' as const;
public readonly isOperational = true;
constructor(operation: string) {
super(`Database connection failed during ${operation} ๐พ`);
this.name = 'DatabaseConnectionError';
}
}
// ๐จ Error response formatter
class ErrorResponseFormatter {
static formatForClient(error: APIError, clientType: 'web' | 'mobile', isDev: boolean) {
const baseResponse = {
success: false,
error: {
code: error.errorCode,
message: this.getUserFriendlyMessage(error, clientType),
timestamp: new Date().toISOString()
}
};
// ๐ง Add debug info in development
if (isDev) {
return {
...baseResponse,
debug: {
originalMessage: error.message,
stack: error.stack,
context: error.context
}
};
}
return baseResponse;
}
private static getUserFriendlyMessage(error: APIError, clientType: 'web' | 'mobile'): string {
const messages = {
web: {
USER_NOT_FOUND: 'The requested user could not be found. Please check the user ID and try again.',
RATE_LIMIT_EXCEEDED: 'Too many requests. Please wait a moment before trying again.',
DATABASE_UNAVAILABLE: 'Our service is temporarily unavailable. Please try again in a few minutes.'
},
mobile: {
USER_NOT_FOUND: 'User not found ๐ค',
RATE_LIMIT_EXCEEDED: 'Too many requests โฐ',
DATABASE_UNAVAILABLE: 'Service unavailable ๐ง'
}
};
return messages[clientType][error.errorCode as keyof typeof messages.web] ||
'An unexpected error occurred. Please try again.';
}
}
// ๐ Error metrics tracker
class ErrorMetrics {
private static errorCounts = new Map<string, number>();
private static lastReset = new Date();
static trackError(error: APIError): void {
const key = `${error.errorCode}_${error.severity}`;
const current = this.errorCounts.get(key) || 0;
this.errorCounts.set(key, current + 1);
// ๐จ Alert on critical errors
if (error.severity === 'critical') {
this.alertCriticalError(error);
}
// ๐ Log metrics every hour
this.maybeLogMetrics();
}
private static alertCriticalError(error: APIError): void {
console.error('๐จ CRITICAL ERROR ALERT:', {
code: error.errorCode,
message: error.message,
context: error.context,
timestamp: new Date().toISOString()
});
// ๐ง Here you would send alerts to your team
}
private static maybeLogMetrics(): void {
const now = new Date();
const hoursSinceReset = (now.getTime() - this.lastReset.getTime()) / (1000 * 60 * 60);
if (hoursSinceReset >= 1) {
console.log('๐ Hourly Error Metrics:', Object.fromEntries(this.errorCounts));
this.errorCounts.clear();
this.lastReset = now;
}
}
}
// ๐ก๏ธ Main error handler
class SocialMediaErrorHandler {
static handleError(
err: APIError,
req: Request,
res: Response,
next: NextFunction
): void {
// ๐ Track the error
ErrorMetrics.trackError(err);
// ๐ Log with context
this.logError(err, req);
// ๐ฏ Get client type from headers
const clientType = req.headers['x-client-type'] === 'mobile' ? 'mobile' : 'web';
const isDev = process.env.NODE_ENV === 'development';
// ๐จ Format response
const response = ErrorResponseFormatter.formatForClient(err, clientType, isDev);
// ๐ค Send response
res.status(err.statusCode).json(response);
}
private static logError(err: APIError, req: Request): void {
const logData = {
timestamp: new Date().toISOString(),
severity: err.severity,
errorCode: err.errorCode,
message: err.message,
request: {
method: req.method,
url: req.url,
userAgent: req.get('User-Agent'),
ip: req.ip,
userId: req.headers['x-user-id']
},
context: err.context
};
// ๐ Log based on severity
switch (err.severity) {
case 'critical':
console.error('๐จ CRITICAL:', JSON.stringify(logData, null, 2));
break;
case 'high':
console.error('๐ด HIGH:', JSON.stringify(logData, null, 2));
break;
case 'medium':
console.warn('๐ก MEDIUM:', JSON.stringify(logData, null, 2));
break;
case 'low':
console.info('๐ข LOW:', JSON.stringify(logData, null, 2));
break;
}
}
}
// ๐ฎ Example usage
app.get('/users/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await findUserById(req.params.id);
if (!user) {
throw new UserNotFoundError(req.params.id, {
requestId: req.headers['x-request-id'],
userAgent: req.get('User-Agent')
});
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
});
// ๐ก๏ธ Register the error handler
app.use(SocialMediaErrorHandler.handleError);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create global error handlers with confidence ๐ช
- โ Avoid common mistakes that crash applications ๐ก๏ธ
- โ Apply best practices in real projects ๐ฏ
- โ Debug issues like a pro ๐
- โ Build resilient backends with TypeScript! ๐
Remember: Error handling isnโt just about catching errors - itโs about creating great user experiences even when things go wrong! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered global error handling in TypeScript!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a project with comprehensive error handling
- ๐ Move on to our next tutorial: โLogging: Structured Logging with Winstonโ
- ๐ Share your error handling strategies with others!
Remember: Every backend expert was once a beginner. Keep coding, keep learning, and most importantly, handle those errors gracefully! ๐
Happy coding! ๐๐โจ