Skip to main content

Error Handling & Debugging

Robust error handling is what separates amateur applications from production-ready systems. In this chapter, you’ll learn how to handle errors gracefully, debug effectively, and implement logging strategies.

Understanding Error Types

Operational vs Programming Errors

TypeDescriptionExamplesHandling
OperationalRuntime problems you can anticipateNetwork failures, invalid user input, database downHandle gracefully, retry, inform user
ProgrammingBugs in your codeTypeError, undefined access, wrong API usageFix the code, crash and restart
Critical Distinction: Operational errors should be handled. Programming errors should crash the app (in production, use a process manager to restart).

The Error Class

// Built-in Error types
const err1 = new Error('Something went wrong');
const err2 = new TypeError('Expected a string');
const err3 = new RangeError('Value out of range');
const err4 = new SyntaxError('Invalid syntax');

// Error properties
console.log(err1.message);  // 'Something went wrong'
console.log(err1.name);     // 'Error'
console.log(err1.stack);    // Stack trace

Custom Error Classes

// Base application error
class AppError extends Error {
  constructor(message, statusCode, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = isOperational;
    
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400);
    this.errors = errors;
  }
}

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

class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403);
  }
}

class ConflictError extends AppError {
  constructor(message = 'Conflict') {
    super(message, 409);
  }
}

// Usage
throw new NotFoundError('User');
throw new ValidationError('Validation failed', [
  { field: 'email', message: 'Invalid email format' }
]);

Async Error Handling Patterns

Callbacks (Legacy)

// Error-first callback pattern
fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    return;
  }
  console.log(data);
});

Promises

// With .catch()
fetchData()
  .then(data => process(data))
  .then(result => save(result))
  .catch(err => {
    console.error('Error:', err);
  });

// Promise.allSettled for multiple operations
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(3)
]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`User ${index + 1}:`, result.value);
  } else {
    console.error(`User ${index + 1} failed:`, result.reason);
  }
});

Async/Await

// Basic try/catch
async function getData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new AppError('Failed to fetch data', response.status);
    }
    return await response.json();
  } catch (error) {
    if (error.isOperational) {
      // Handle operational error
      console.error('Operational error:', error.message);
    } else {
      // Re-throw programming errors
      throw error;
    }
  }
}

// Wrapper function for cleaner code
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage in Express
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) throw new NotFoundError('User');
  res.json(user);
}));

Express Error Handling

Centralized Error Handler

// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
  let error = { ...err };
  error.message = err.message;
  error.stack = err.stack;

  // Log error
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    url: req.originalUrl,
    method: req.method,
    ip: req.ip,
    userId: req.user?.id
  });

  // Mongoose bad ObjectId
  if (err.name === 'CastError') {
    error = new AppError('Invalid ID format', 400);
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    error = new ConflictError(`${field} already exists`);
  }

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message
    }));
    error = new ValidationError('Validation failed', errors);
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    error = new UnauthorizedError('Invalid token');
  }

  if (err.name === 'TokenExpiredError') {
    error = new UnauthorizedError('Token expired');
  }

  // Development vs Production responses
  if (process.env.NODE_ENV === 'development') {
    res.status(error.statusCode || 500).json({
      success: false,
      error: error.message,
      stack: error.stack,
      errors: error.errors
    });
  } else {
    // Production: Don't leak error details
    if (error.isOperational) {
      res.status(error.statusCode).json({
        success: false,
        error: error.message,
        ...(error.errors && { errors: error.errors })
      });
    } else {
      // Programming error: send generic message
      console.error('UNEXPECTED ERROR:', err);
      res.status(500).json({
        success: false,
        error: 'Something went wrong'
      });
    }
  }
};

module.exports = errorHandler;

404 Handler

// Handle undefined routes
app.use((req, res, next) => {
  next(new NotFoundError(`Route ${req.originalUrl} not found`));
});

// Error handler (must be last)
app.use(errorHandler);

Global Error Handlers

// Uncaught exceptions (synchronous code)
process.on('uncaughtException', (err) => {
  console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
  console.error(err.name, err.message);
  console.error(err.stack);
  process.exit(1);
});

// Unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('UNHANDLED REJECTION! 💥 Shutting down...');
  console.error(reason);
  
  // Close server gracefully, then exit
  server.close(() => {
    process.exit(1);
  });
});

// SIGTERM handling (Heroku, Docker, etc.)
process.on('SIGTERM', () => {
  console.log('👋 SIGTERM received. Shutting down gracefully');
  server.close(() => {
    console.log('💥 Process terminated');
  });
});

Debugging Techniques

Console Methods

// Basic logging
console.log('Info message');
console.error('Error message');
console.warn('Warning message');

// Formatted output
console.table([
  { name: 'John', age: 25 },
  { name: 'Jane', age: 30 }
]);

// Timing
console.time('operation');
// ... some operation
console.timeEnd('operation'); // operation: 123ms

// Stack trace
console.trace('Trace point');

// Assertions
console.assert(1 === 2, 'Values are not equal');

// Grouping
console.group('User Data');
console.log('Name: John');
console.log('Age: 25');
console.groupEnd();

Node.js Debugger

# Built-in debugger
node inspect app.js

# Chrome DevTools
node --inspect app.js
# Then open chrome://inspect in Chrome

# Break on first line
node --inspect-brk app.js

VS Code Debugging

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug App",
      "program": "${workspaceFolder}/src/server.js",
      "env": {
        "NODE_ENV": "development"
      },
      "restart": true,
      "console": "integratedTerminal"
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Attach to Process",
      "port": 9229
    }
  ]
}

Logging with Winston

npm install winston
// config/logger.js
const winston = require('winston');
const path = require('path');

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4
};

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'blue'
};

winston.addColors(colors);

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
  winston.format.errors({ stack: true }),
  winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
    let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
    if (Object.keys(meta).length) {
      log += ` ${JSON.stringify(meta)}`;
    }
    if (stack) {
      log += `\n${stack}`;
    }
    return log;
  })
);

const transports = [
  // Console output
  new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize({ all: true }),
      format
    )
  }),
  
  // Error log file
  new winston.transports.File({
    filename: path.join('logs', 'error.log'),
    level: 'error',
    maxsize: 5242880, // 5MB
    maxFiles: 5
  }),
  
  // Combined log file
  new winston.transports.File({
    filename: path.join('logs', 'combined.log'),
    maxsize: 5242880,
    maxFiles: 5
  })
];

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  levels,
  format,
  transports,
  exitOnError: false
});

module.exports = logger;

HTTP Request Logging with Morgan

const morgan = require('morgan');
const logger = require('./config/logger');

// Stream for Morgan to use Winston
const stream = {
  write: (message) => logger.http(message.trim())
};

// Custom token
morgan.token('body', (req) => JSON.stringify(req.body));

// Use in Express
app.use(morgan(
  ':method :url :status :res[content-length] - :response-time ms',
  { stream }
));

Structured Logging

// Log with context
logger.info('User logged in', {
  userId: user.id,
  email: user.email,
  ip: req.ip,
  userAgent: req.headers['user-agent']
});

logger.error('Database connection failed', {
  host: dbConfig.host,
  error: err.message
});

// Request context middleware
const requestLogger = (req, res, next) => {
  req.logger = logger.child({
    requestId: req.headers['x-request-id'] || uuid(),
    path: req.path,
    method: req.method
  });
  next();
};

// Usage
app.get('/users', (req, res) => {
  req.logger.info('Fetching users');
  // ...
});

Summary

  • Distinguish between operational and programming errors
  • Create custom error classes for different error types
  • Use async wrappers to catch errors in async routes
  • Implement a centralized error handler in Express
  • Handle uncaught exceptions and unhandled rejections
  • Use Winston for structured, configurable logging
  • Debug with VS Code or Chrome DevTools
  • Log meaningful context with each error