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.
typescript
// types/errors.ts
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400);
}
}
export class AuthenticationError extends AppError {
constructor(message = "Authentication failed") {
super(message, 401);
}
}
export class AuthorizationError extends AppError {
constructor(message = "Insufficient permissions") {
super(message, 403);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(${resource} not found, 404);
}
}
export class DatabaseError extends AppError {
constructor(message: string) {
super(message, 500, false); // Not operational
}
}
Global Error Handler (Express/Next.js API)
typescript
// middleware/error-handler.ts
import { NextRequest, NextResponse } from "next/server";
import { AppError } from "@/types/errors";
import { logger } from "@/lib/logger";
export function errorHandler(error: Error, request: NextRequest) {
// Log error
logger.error({
message: error.message,
stack: error.stack,
url: request.url,
method: request.method,
});
// Handle known errors
if (error instanceof AppError) {
return NextResponse.json(
{
status: "error",
message: error.message,
},
{ status: error.statusCode }
);
}
// Handle unexpected errors (don't expose details)
return NextResponse.json(
{
status: "error",
message: "An unexpected error occurred",
},
{ status: 500 }
);
}
// Usage in API route
export async function POST(request: NextRequest) {
try {
const body = await request.json();
if (!body.email) {
throw new ValidationError("Email is required");
}
// Process request...
return NextResponse.json({ success: true });
} catch (error) {
return errorHandler(error as Error, request);
}
}
Try-Catch Best Practices
typescript
// Bad ❌
async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } });
return user;
}
// Good ✅
async function getUser(id: string) {
try {
const user = await db.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundError("User");
}
return user;
} catch (error) {
if (error instanceof AppError) {
throw error;
}
// Unexpected database error
logger.error("Database error in getUser:", error);
throw new DatabaseError("Failed to fetch user");
}
}
// Usage with proper error handling
async function handleRequest() {
try {
const user = await getUser("123");
return { success: true, user };
} catch (error) {
if (error instanceof NotFoundError) {
return { success: false, message: "User not found" };
}
if (error instanceof DatabaseError) {
return { success: false, message: "Service temporarily unavailable" };
}
throw error; // Re-throw unexpected errors
}
}
React Error Boundaries
typescript
// components/error-boundary.tsx
"use client";
import { Component, ReactNode } from "react";
import { logger } from "@/lib/logger";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
logger.error({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
Something went wrong
We're sorry for the inconvenience. Please refresh the page or try
again later.
)
);
}
return this.props.children;
}
}
// Usage
;
Structured Logging
typescript
// lib/logger.ts
type LogLevel = "debug" | "info" | "warn" | "error";
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
[key: string]: any;
}
class Logger {
private isDevelopment = process.env.NODE_ENV === "development";
private formatLog(level: LogLevel, message: string, meta?: any): LogEntry {
return {
level,
message,
timestamp: new Date().toISOString(),
...meta,
};
}
private write(entry: LogEntry) {
if (this.isDevelopment) {
// Pretty print in development
consoleentry.level;
} else {
// JSON format for production (easy to parse)
console.log(JSON.stringify(entry));
}
}
debug(message: string, meta?: any) {
this.write(this.formatLog("debug", message, meta));
}
info(message: string, meta?: any) {
this.write(this.formatLog("info", message, meta));
}
warn(message: string, meta?: any) {
this.write(this.formatLog("warn", message, meta));
}
error(message: string | object, meta?: any) {
const errorMessage =
typeof message === "string"
? message
: message.message || "Error occurred";
this.write(this.formatLog("error", errorMessage, { ...meta, ...message }));
}
}
export const logger = new Logger();
// Usage
logger.info("User logged in", { userId: "123", ip: "192.168.1.1" });
logger.error("Payment failed", { orderId: "456", amount: 99.99, error: err });
Async Error Handling
typescript
// lib/async-handler.ts
export function asyncHandler(fn: Function) {
return async (...args: any[]) => {
try {
return await fn(...args);
} catch (error) {
logger.error("Async operation failed", { error });
throw error;
}
};
}
// Usage
const fetchUser = asyncHandler(async (id: string) => {
const response = await fetch(/api/users/${id});
if (!response.ok) throw new Error("Failed to fetch user");
return response.json();
});
Promise Error Handling
typescript
// Handle multiple promises
async function fetchAllData() {
const [users, posts, comments] = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);
return {
users: users.status === "fulfilled" ? users.value : [],
posts: posts.status === "fulfilled" ? posts.value : [],
comments: comments.status === "fulfilled" ? comments.value : [],
errors: [users, posts, comments]
.filter((result) => result.status === "rejected")
.map((result) => (result as PromiseRejectedResult).reason),
};
}
Validation Error Handling
typescript
// lib/validate.ts
import { z } from "zod";
export function validateRequest(schema: z.Schema, data: unknown): T {
try {
return schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.errors.map(
(err) => ${err.path.join(".")}: ${err.message}
);
throw new ValidationError(messages.join(", "));
}
throw error;
}
}
// Usage
const userSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validData = validateRequest(userSchema, body);
// Process valid data...
return NextResponse.json({ success: true });
} catch (error) {
return errorHandler(error as Error, request);
}
}
Monitoring Integration
typescript
// lib/monitoring.ts
class ErrorMonitoring {
captureException(error: Error, context?: any) {
// Send to monitoring service (Sentry, Datadog, etc.)
if (process.env.NODE_ENV === "production") {
// Sentry.captureException(error, { extra: context })
console.error("Error captured:", error, context);
}
}
captureMessage(message: string, level: "info" | "warning" | "error") {
if (process.env.NODE_ENV === "production") {
// Sentry.captureMessage(message, level)
console.log([${level}], message);
}
}
setUser(user: { id: string; email: string }) {
// Associate errors with user
// Sentry.setUser(user)
}
}
export const monitoring = new ErrorMonitoring();
Client-Side Error Handling
typescript
// app/error.tsx (Next.js)
"use client";
import { useEffect } from "react";
import { logger } from "@/lib/logger";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
logger.error("Client-side error", {
message: error.message,
digest: error.digest,
});
}, [error]);
return (
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
typescript
// Standard error response
interface ErrorResponse {
status: 'error'
message: string
code?: string
details?: any[]
}
// Example
{
status: 'error',
message: 'Validation failed',
code: 'VALIDATION_ERROR',
details: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: '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! 🛡️