Skip to main content
โšก Calmops

Error Handling and Logging

Error Handling and Logging

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');
    
  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');
    
  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

Comments