Back to blog

Error Handling and Logging Best Practices for Production Applications

8 min readBy Mustafa Akkaya
#Error Handling#Logging#Monitoring#Node.js#Next.js#Best Practices

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.

app.ts

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)

app.ts

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

app.ts

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

app.ts

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

app.ts

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

consoleclass="keyword">class="string">"entry.message, entry" target=class="keyword">class="string">"_blank" rel=class="keyword">class="string">"noopener noreferrer" class="keyword">class=class="keyword">class="string">"font-semibold text-indigo-600 transition-colors hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300">entry.level;

} 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

app.ts

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

app.ts

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

app.ts

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

app.ts

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

app.ts

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

app.ts

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! 🛡️