Skip to main content

Error Handling and Logging

Created: May 8, 2026 Larry Qu 6 min read

Error handling and logging are critical for maintaining production applications. This article covers best practices.

Introduction

Error handling and logging provide:

  • Error tracking
  • Debugging capabilities
  • Performance monitoring
  • Audit trails
  • System reliability

Understanding error handling helps you:

  • Catch and handle errors
  • Debug issues
  • Monitor applications
  • Improve reliability
  • Maintain code quality

Error Handling Strategies

Try-Catch Blocks

// ✅ Good: Basic try-catch
try {
  const data = JSON.parse(jsonString);
  console.log(data);
} catch (err) {
  console.error('Error parsing JSON:', err.message);
}

// ✅ Good: Try-catch with finally
try {
  const file = fs.readFileSync('file.txt', 'utf-8');
  console.log(file);
} catch (err) {
  console.error('Error reading file:', err);
} finally {
  console.log('Operation completed');
}

// ✅ Good: Async try-catch
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (err) {
    console.error('Error fetching data:', err);
    throw err;
  }
}

// ✅ Good: Multiple catch blocks
try {
  // Some operation
} catch (err) {
  if (err instanceof TypeError) {
    console.error('Type error:', err);
  } else if (err instanceof ReferenceError) {
    console.error('Reference error:', err);
  } else {
    console.error('Unknown error:', err);
  }
}

Custom Error Classes

// ✅ Good: Custom error class
class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
    this.timestamp = new Date();
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends AppError {
  constructor(message) {
    super(message, 404);
    this.name = 'NotFoundError';
  }
}

class UnauthorizedError extends AppError {
  constructor(message) {
    super(message, 401);
    this.name = 'UnauthorizedError';
  }
}

// ✅ Good: Throw custom errors
function validateUser(user) {
  if (!user.email) {
    throw new ValidationError('Email is required');
  }
  if (!user.password) {
    throw new ValidationError('Password is required');
  }
}

async function getUser(id) {
  const user = await User.findById(id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  return user;
}

module.exports = { AppError, ValidationError, NotFoundError, UnauthorizedError };

Error Handling Middleware

// middleware/errorHandler.js
const { AppError } = require('./errors');

const errorHandler = (err, req, res, next) => {
  console.error('Error:', err);

  // Default error
  let status = err.status || 500;
  let message = err.message || 'Internal server error';

  // Handle specific errors
  if (err.name === 'ValidationError') {
    status = 400;
  } else if (err.name === 'CastError') {
    status = 400;
    message = 'Invalid ID format';
  } else if (err.name === 'MongoError' && err.code === 11000) {
    status = 400;
    message = 'Duplicate field value';
  }

  res.status(status).json({
    error: {
      status,
      message,
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
};

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = { errorHandler, asyncHandler };

// routes/users.js
const { asyncHandler } = require('../middleware/errorHandler');
const { NotFoundError } = require('../errors');

router.get('/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  res.json(user);
}));

// app.js
const { errorHandler } = require('./middleware/errorHandler');

app.use('/api', routes);
app.use(errorHandler);

Logging

Console Logging

// ✅ Good: Different log levels
console.log('Info message');      // General information
console.warn('Warning message');  // Warning
console.error('Error message');   // Error

// ✅ Good: Structured logging
console.log(JSON.stringify({
  timestamp: new Date(),
  level: 'info',
  message: 'User logged in',
  userId: 123
}));

// ✅ Good: Log with context
function logRequest(req, res, next) {
  console.log({
    timestamp: new Date(),
    method: req.method,
    url: req.url,
    ip: req.ip
  });
  next();
}

Winston Logger

// ✅ Good: Install Winston
// npm install winston

const winston = require('winston');

// ✅ Good: Create logger
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// ✅ Good: Add console transport in development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

// ✅ Good: Use logger
logger.info('Application started');
logger.warn('Warning message');
logger.error('Error occurred', { error: err });

// ✅ Good: Logger middleware
function loggerMiddleware(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip
    });
  });

  next();
}

module.exports = logger;

Morgan Logger

// ✅ Good: Install Morgan
// npm install morgan

const morgan = require('morgan');
const fs = require('fs');
const path = require('path');

const app = express();

// ✅ Good: Use Morgan middleware
app.use(morgan('combined'));

// ✅ Good: Log to file
const accessLogStream = fs.createWriteStream(
  path.join(__dirname, 'access.log'),
  { flags: 'a' }
);

app.use(morgan('combined', { stream: accessLogStream }));

// ✅ Good: Custom Morgan format
morgan.token('user-id', (req) => req.user?.id || 'anonymous');

app.use(morgan(':user-id :method :url :status :response-time ms'));

Debugging

Debug Module

// ✅ Good: Install debug
// npm install debug

const debug = require('debug')('app:*');

// ✅ Good: Use debug
debug('Application started');
debug('User logged in:', userId);
debug('Database query:', query);

// ✅ Good: Namespace debugging
const debugAuth = require('debug')('app:auth');
const debugDB = require('debug')('app:db');

debugAuth('User authentication');
debugDB('Database connection');

// Run with: DEBUG=app:* node app.js
// Or: DEBUG=app:auth node app.js

Error Tracking Services

// ✅ Good: Install Sentry
// npm install @sentry/node

const Sentry = require('@sentry/node');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0
});

const app = express();

app.use(Sentry.Handlers.requestHandler());

// Routes
app.get('/', (req, res) => {
  res.json({ message: 'Hello' });
});

app.use(Sentry.Handlers.errorHandler());

// ✅ Good: Capture exceptions
try {
  // Some operation
} catch (err) {
  Sentry.captureException(err);
}

// ✅ Good: Capture messages
Sentry.captureMessage('Something went wrong', 'warning');

Monitoring and Alerting

Health Checks

// ✅ Good: Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date(),
    uptime: process.uptime()
  });
});

// ✅ Good: Detailed health check
app.get('/health/detailed', async (req, res) => {
  try {
    const dbHealth = await checkDatabaseHealth();
    const cacheHealth = await checkCacheHealth();

    res.json({
      status: 'ok',
      database: dbHealth,
      cache: cacheHealth,
      timestamp: new Date()
    });
  } catch (err) {
    res.status(503).json({
      status: 'error',
      error: err.message
    });
  }
});

// ✅ Good: Readiness check
app.get('/ready', async (req, res) => {
  try {
    await checkDatabaseConnection();
    res.json({ ready: true });
  } catch (err) {
    res.status(503).json({ ready: false });
  }
});

Best Practices

  1. Log at appropriate levels:
    // ✅ Good: Appropriate log levels
    logger.info('User logged in');
    logger.warn('Deprecated API used');
    logger.error('Database connection failed');
    
    // ❌ Bad: Wrong log levels
    logger.error('User logged in');
    logger.info('Database connection failed');
    ```javascript
    
  2. Include context in logs:
    // ✅ Good: Include context
    logger.error('Error processing request', {
      userId: req.user?.id,
      requestId: req.id,
      error: err.message
    });
    
    // ❌ Bad: No context
    logger.error('Error occurred');
    ```javascript
    
  3. Handle errors gracefully:
    // ✅ Good: Graceful error handling
    try {
      await operation();
    } catch (err) {
      logger.error('Operation failed', err);
      res.status(500).json({ error: 'Operation failed' });
    }
    
    // ❌ Bad: Crash on error
    await operation();
    

Summary

Error handling and logging are essential. Key takeaways:

  • Use try-catch for error handling
  • Create custom error classes
  • Implement error handling middleware
  • Use logging frameworks
  • Monitor applications
  • Include context in logs
  • Handle errors gracefully
  • Track errors in production

Next Steps

Resources

Comments

Share this article

Scan to read on mobile