Error Handling and Logging Best Practices for Production Applications
Error handling and logging are critical for production applications. Well-implemented error management helps you catch bugs early, debug issues faster, and provide better user experiences. Let's explore practical patterns for robust error handling.
Why Error Handling Matters
Poor error handling leads to:
- Poor User Experience - Cryptic error messages, app crashes
- Security Vulnerabilities - Exposed stack traces, sensitive data leaks
- Lost Context - Missing information for debugging
- Downtime - Unhandled errors crashing the application
Good error handling provides:
- Graceful Degradation - App continues working with limited functionality
- Clear Feedback - Users understand what went wrong
- Actionable Logs - Developers can debug and fix issues quickly
- Monitoring Insights - Track error trends and patterns
Error Types
Understanding error categories helps handle them appropriately.
class="keyword">class="comment">// types/errors.ts
class="keyword">export class="keyword">class AppError class="keyword">extends Error {
constructor(
class="keyword">public message: string,
class="keyword">public statusCode: number,
class="keyword">public isOperational: boolean = true
) {
class="keyword">super(message);
class="keyword">this.name = class="keyword">this.constructor.name;
Error.captureStackTrace(class="keyword">this, class="keyword">this.constructor);
}
}
class="keyword">export class="keyword">class ValidationError class="keyword">extends AppError {
constructor(message: string) {
class="keyword">super(message, 400);
}
}
class="keyword">export class="keyword">class AuthenticationError class="keyword">extends AppError {
constructor(message = class="keyword">class="string">"Authentication failed") {
class="keyword">super(message, 401);
}
}
class="keyword">export class="keyword">class AuthorizationError class="keyword">extends AppError {
constructor(message = class="keyword">class="string">"Insufficient permissions") {
class="keyword">super(message, 403);
}
}
class="keyword">export class="keyword">class NotFoundError class="keyword">extends AppError {
constructor(resource: string) {
class="keyword">super(class="keyword">class="string">${resource} not found, 404);
}
}
class="keyword">export class="keyword">class DatabaseError class="keyword">extends AppError {
constructor(message: string) {
class="keyword">super(message, 500, false); class="keyword">class="comment">// Not operational
}
}
Global Error Handler (Express/Next.js API)
class="keyword">class="comment">// middleware/error-handler.ts
class="keyword">import { NextRequest, NextResponse } class="keyword">from class="keyword">class="string">"next/server";
class="keyword">import { AppError } class="keyword">from class="keyword">class="string">"@/types/errors";
class="keyword">import { logger } class="keyword">from class="keyword">class="string">"@/lib/logger";
class="keyword">export class="keyword">class="keyword">function errorHandler(error: Error, request: NextRequest) {
class="keyword">class="comment">// Log error
logger.error({
message: error.message,
stack: error.stack,
url: request.url,
method: request.method,
});
class="keyword">class="comment">// Handle known errors
class="keyword">class="keyword">if (error class="keyword">instanceof AppError) {
class="keyword">class="keyword">return NextResponse.json(
{
status: class="keyword">class="string">"error",
message: error.message,
},
{ status: error.statusCode }
);
}
class="keyword">class="comment">// Handle unexpected errors(don't expose details)
class="keyword">class="keyword">return NextResponse.json(
{
status: class="keyword">class="string">"error",
message: class="keyword">class="string">"An unexpected error occurred",
},
{ status: 500 }
);
}
class="keyword">class="comment">// Usage in API route
class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function POST(request: NextRequest) {
class="keyword">try {
class="keyword">class="keyword">const body = class="keyword">class="keyword">await request.json();
class="keyword">class="keyword">if (!body.email) {
class="keyword">throw class="keyword">new ValidationError(class="keyword">class="string">"Email is required");
}
class="keyword">class="comment">// Process request...
class="keyword">class="keyword">return NextResponse.json({ success: true });
} class="keyword">catch (error) {
class="keyword">class="keyword">return errorHandler(error as Error, request);
}
}
Try-Catch Best Practices
class="keyword">class="comment">// Bad ❌
class="keyword">class="keyword">async class="keyword">class="keyword">function getUser(id: string) {
class="keyword">class="keyword">const user = class="keyword">class="keyword">await db.user.findUnique({ where: { id } });
class="keyword">class="keyword">return user;
}
class="keyword">class="comment">// Good ✅
class="keyword">class="keyword">async class="keyword">class="keyword">function getUser(id: string) {
class="keyword">try {
class="keyword">class="keyword">const user = class="keyword">class="keyword">await db.user.findUnique({ where: { id } });
class="keyword">class="keyword">if (!user) {
class="keyword">throw class="keyword">new NotFoundError(class="keyword">class="string">"User");
}
class="keyword">class="keyword">return user;
} class="keyword">catch (error) {
class="keyword">class="keyword">if (error class="keyword">instanceof AppError) {
class="keyword">throw error;
}
class="keyword">class="comment">// Unexpected database error
logger.error(class="keyword">class="string">"Database error in getUser:", error);
class="keyword">throw class="keyword">new DatabaseError(class="keyword">class="string">"Failed to fetch user");
}
}
class="keyword">class="comment">// Usage with proper error handling
class="keyword">class="keyword">async class="keyword">class="keyword">function handleRequest() {
class="keyword">try {
class="keyword">class="keyword">const user = class="keyword">class="keyword">await getUser(class="keyword">class="string">"123");
class="keyword">class="keyword">return { success: true, user };
} class="keyword">catch (error) {
class="keyword">class="keyword">if (error class="keyword">instanceof NotFoundError) {
class="keyword">class="keyword">return { success: false, message: class="keyword">class="string">"User not found" };
}
class="keyword">class="keyword">if (error class="keyword">instanceof DatabaseError) {
class="keyword">class="keyword">return { success: false, message: class="keyword">class="string">"Service temporarily unavailable" };
}
class="keyword">throw error; class="keyword">class="comment">// Re-class="keyword">throw unexpected errors
}
}
React Error Boundaries
class="keyword">class="comment">// components/error-boundary.tsx
class="keyword">class="string">"use client";
class="keyword">import { Component, ReactNode } class="keyword">from class="keyword">class="string">"react";
class="keyword">import { logger } class="keyword">from class="keyword">class="string">"@/lib/logger";
class="keyword">interface Props {
children: ReactNode;
fallback?: ReactNode;
}
class="keyword">interface State {
hasError: boolean;
error: Error | class="keyword">null;
}
class="keyword">export class="keyword">class ErrorBoundary class="keyword">extends Component {
constructor(props: Props) {
class="keyword">super(props);
class="keyword">this.state = { hasError: false, error: class="keyword">null };
}
class="keyword">static getDerivedStateFromError(error: Error): State {
class="keyword">class="keyword">return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
logger.error({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
class="keyword">class="keyword">if (class="keyword">this.state.hasError) {
class="keyword">class="keyword">return (
class="keyword">this.props.fallback || (
class="keyword">class="string">"p-6 bg-red-50 border border-red-200 rounded-lg">
class="keyword">class="string">"text-xl font-bold text-red-800 mb-2">
Something went wrong
class="keyword">class="string">"text-red-600">
We're sorry class="keyword">class="keyword">for the inconvenience. Please refresh the page or class="keyword">try
again later.
)
);
}
class="keyword">class="keyword">return class="keyword">this.props.children;
}
}
class="keyword">class="comment">// Usage
;
Structured Logging
class="keyword">class="comment">// lib/logger.ts
class="keyword">type LogLevel = class="keyword">class="string">"debug" | class="keyword">class="string">"info" | class="keyword">class="string">"warn" | class="keyword">class="string">"error";
class="keyword">interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
[key: string]: any;
}
class="keyword">class Logger {
class="keyword">private isDevelopment = process.env.NODE_ENV === class="keyword">class="string">"development";
class="keyword">private formatLog(level: LogLevel, message: string, meta?: any): LogEntry {
class="keyword">class="keyword">return {
level,
message,
timestamp: class="keyword">new Date().toISOString(),
...meta,
};
}
class="keyword">private write(entry: LogEntry) {
class="keyword">class="keyword">if (class="keyword">this.isDevelopment) {
class="keyword">class="comment">// Pretty print in development
} class="keyword">class="keyword">else {
class="keyword">class="comment">// JSON format class="keyword">class="keyword">for production(easy to parse)
console.log(JSON.stringify(entry));
}
}
debug(message: string, meta?: any) {
class="keyword">this.write(class="keyword">this.formatLog(class="keyword">class="string">"debug", message, meta));
}
info(message: string, meta?: any) {
class="keyword">this.write(class="keyword">this.formatLog(class="keyword">class="string">"info", message, meta));
}
warn(message: string, meta?: any) {
class="keyword">this.write(class="keyword">this.formatLog(class="keyword">class="string">"warn", message, meta));
}
error(message: string | object, meta?: any) {
class="keyword">class="keyword">const errorMessage =
class="keyword">typeof message === class="keyword">class="string">"string"
? message
: message.message || class="keyword">class="string">"Error occurred";
class="keyword">this.write(class="keyword">this.formatLog(class="keyword">class="string">"error", errorMessage, { ...meta, ...message }));
}
}
class="keyword">export class="keyword">class="keyword">const logger = class="keyword">new Logger();
class="keyword">class="comment">// Usage
logger.info(class="keyword">class="string">"User logged in", { userId: class="keyword">class="string">"123", ip: class="keyword">class="string">"192.168.1.1" });
logger.error(class="keyword">class="string">"Payment failed", { orderId: class="keyword">class="string">"456", amount: 99.99, error: err });
Async Error Handling
class="keyword">class="comment">// lib/class="keyword">class="keyword">async-handler.ts
class="keyword">export class="keyword">class="keyword">function asyncHandler(fn: Function) {
class="keyword">class="keyword">return class="keyword">class="keyword">async (...args: any[]) => {
class="keyword">try {
class="keyword">class="keyword">return class="keyword">class="keyword">await fn(...args);
} class="keyword">catch (error) {
logger.error(class="keyword">class="string">"Async operation failed", { error });
class="keyword">throw error;
}
};
}
class="keyword">class="comment">// Usage
class="keyword">class="keyword">const fetchUser = asyncHandler(class="keyword">class="keyword">async (id: string) => {
class="keyword">class="keyword">const response = class="keyword">class="keyword">await fetch(class="keyword">class="string">/api/users/${id});
class="keyword">class="keyword">if (!response.ok) class="keyword">throw class="keyword">new Error(class="keyword">class="string">"Failed to fetch user");
class="keyword">class="keyword">return response.json();
});
Promise Error Handling
class="keyword">class="comment">// Handle multiple promises
class="keyword">class="keyword">async class="keyword">class="keyword">function fetchAllData() {
class="keyword">class="keyword">const [users, posts, comments] = class="keyword">class="keyword">await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
class="keyword">class="keyword">return {
users: users.status === class="keyword">class="string">"fulfilled" ? users.value : [],
posts: posts.status === class="keyword">class="string">"fulfilled" ? posts.value : [],
comments: comments.status === class="keyword">class="string">"fulfilled" ? comments.value : [],
errors: [users, posts, comments]
.filter((result) => result.status === class="keyword">class="string">"rejected")
.map((result) => (result as PromiseRejectedResult).reason),
};
}
Validation Error Handling
class="keyword">class="comment">// lib/validate.ts
class="keyword">import { z } class="keyword">from class="keyword">class="string">"zod";
class="keyword">export class="keyword">class="keyword">function validateRequest(schema: z.Schema, data: unknown): T {
class="keyword">try {
class="keyword">class="keyword">return schema.parse(data);
} class="keyword">catch (error) {
class="keyword">class="keyword">if (error class="keyword">instanceof z.ZodError) {
class="keyword">class="keyword">const messages = error.errors.map(
(err) => class="keyword">class="string">${err.path.join(".")}: ${err.message}
);
class="keyword">throw class="keyword">new ValidationError(messages.join(class="keyword">class="string">", "));
}
class="keyword">throw error;
}
}
class="keyword">class="comment">// Usage
class="keyword">class="keyword">const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
class="keyword">export class="keyword">class="keyword">async class="keyword">class="keyword">function POST(request: NextRequest) {
class="keyword">try {
class="keyword">class="keyword">const body = class="keyword">class="keyword">await request.json();
class="keyword">class="keyword">const validData = validateRequest(userSchema, body);
class="keyword">class="comment">// Process valid data...
class="keyword">class="keyword">return NextResponse.json({ success: true });
} class="keyword">catch (error) {
class="keyword">class="keyword">return errorHandler(error as Error, request);
}
}
Monitoring Integration
class="keyword">class="comment">// lib/monitoring.ts
class="keyword">class ErrorMonitoring {
captureException(error: Error, context?: any) {
class="keyword">class="comment">// Send to monitoring service(Sentry, Datadog, etc.)
class="keyword">class="keyword">if (process.env.NODE_ENV === class="keyword">class="string">"production") {
class="keyword">class="comment">// Sentry.captureException(error, { extra: context })
console.error(class="keyword">class="string">"Error captured:", error, context);
}
}
captureMessage(message: string, level: class="keyword">class="string">"info" | class="keyword">class="string">"warning" | class="keyword">class="string">"error") {
class="keyword">class="keyword">if (process.env.NODE_ENV === class="keyword">class="string">"production") {
class="keyword">class="comment">// Sentry.captureMessage(message, level)
console.log(class="keyword">class="string">[${level}], message);
}
}
setUser(user: { id: string; email: string }) {
class="keyword">class="comment">// Associate errors with user
class="keyword">class="comment">// Sentry.setUser(user)
}
}
class="keyword">export class="keyword">class="keyword">const monitoring = class="keyword">new ErrorMonitoring();
Client-Side Error Handling
class="keyword">class="comment">// app/error.tsx(Next.js)
class="keyword">class="string">"use client";
class="keyword">import { useEffect } class="keyword">from class="keyword">class="string">"react";
class="keyword">import { logger } class="keyword">from class="keyword">class="string">"@/lib/logger";
class="keyword">export class="keyword">default class="keyword">class="keyword">function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => class="keyword">void;
}) {
useEffect(() => {
logger.error(class="keyword">class="string">"Client-side error", {
message: error.message,
digest: error.digest,
});
}, [error]);
class="keyword">class="keyword">return (
class="keyword">class="string">"flex flex-col items-center justify-center min-h-screen">
class="keyword">class="string">"text-2xl font-bold mb-4">Something went wrong!
);
}
Best Practices
1. Never expose stack traces in production responses
2. Log contextual information - user ID, request ID, timestamp
3. Use specific error types - Easier to handle and monitor
4. Fail fast - Validate early, throw immediately
5. Don't swallow errors - Always log or re-throw
6. Centralize error handling - Single source of truth
7. Monitor errors - Use Sentry, Datadog, or similar
8. Set up alerts - Get notified of critical errors
9. Test error scenarios - Write tests for error cases
10. Document errors - Help team understand error patterns
Error Response Format
class="keyword">class="comment">// Standard error response
class="keyword">interface ErrorResponse {
status: class="keyword">class="string">'error'
message: string
code?: string
details?: any[]
}
class="keyword">class="comment">// Example
{
status: class="keyword">class="string">'error',
message: class="keyword">class="string">'Validation failed',
code: class="keyword">class="string">'VALIDATION_ERROR',
details: [
{ field: class="keyword">class="string">'email', message: class="keyword">class="string">'Invalid email format' },
{ field: class="keyword">class="string">'password', message: class="keyword">class="string">'Password too short' }
]
}
Conclusion
Robust error handling is not optional—it's essential for production applications. Implement proper error types, centralized logging, and monitoring to catch issues early. Remember: errors will happen, but how you handle them defines the reliability of your application.
Good error handling turns failures into learning opportunities! 🛡️