> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 14. Error Handling & Debugging

> Master error handling patterns, debugging techniques, and logging in Node.js applications.

# 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

| Type            | Description                         | Examples                                            | Handling                              |
| --------------- | ----------------------------------- | --------------------------------------------------- | ------------------------------------- |
| **Operational** | Runtime problems you can anticipate | Network failures, invalid user input, database down | Handle gracefully, retry, inform user |
| **Programming** | Bugs in your code                   | TypeError, undefined access, wrong API usage        | Fix the code, crash and restart       |

<Warning>
  **Critical Distinction**: Operational errors should be handled. Programming errors should crash the app (in production, use a process manager to restart).
</Warning>

## The Error Class

Every error in JavaScript inherits from the built-in `Error` class. When you create an error, Node.js automatically captures a **stack trace** -- a breadcrumb trail showing exactly which functions were called and in what order leading up to the error. This stack trace is your most valuable debugging tool.

```javascript theme={null}
// Built-in Error types -- each signals a different category of problem
const err1 = new Error('Something went wrong');     // Generic error
const err2 = new TypeError('Expected a string');     // Wrong type used
const err3 = new RangeError('Value out of range');   // Number outside valid range
const err4 = new SyntaxError('Invalid syntax');      // Malformed code or JSON

// Error properties
console.log(err1.message);  // 'Something went wrong' -- human-readable description
console.log(err1.name);     // 'Error' -- the constructor name
console.log(err1.stack);    // Stack trace -- shows the call chain that led here
```

## Custom Error Classes

```javascript theme={null}
// Base application error -- this is the foundation of a custom error hierarchy.
// The key insight: by adding statusCode and isOperational, your error handler
// can distinguish "expected" errors (bad user input, missing resource) from
// "unexpected" ones (null pointer, out of memory) and respond appropriately.
class AppError extends Error {
  constructor(message, statusCode, isOperational = true) {
    super(message);
    this.statusCode = statusCode;
    // Derive 'fail' (client error, 4xx) vs 'error' (server error, 5xx)
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    // isOperational = true means "we expected this could happen"
    // isOperational = false means "this is a bug in our code"
    this.isOperational = isOperational;
    
    // Exclude this constructor from the stack trace so the trace
    // points to where the error was thrown, not where it was constructed
    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)

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

### Promises

```javascript theme={null}
// 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

```javascript theme={null}
// 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 -- eliminates the need for try/catch
// in every single route handler. This is a higher-order function: it takes
// your async handler, runs it, and if the returned promise rejects,
// automatically forwards the error to Express's next().
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

```javascript theme={null}
// 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

```javascript theme={null}
// 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

These are your application's last line of defense -- safety nets that catch errors which slipped through every other handler. In a well-designed app, these should rarely fire. If they fire frequently, you have unhandled errors upstream that need fixing.

Think of these like the emergency shutoff on a factory machine: you hope they never activate, but when they do, they prevent catastrophic damage.

```javascript theme={null}
// Uncaught exceptions (synchronous code) -- fires when a throw statement
// reaches the top of the call stack without being caught.
// The process is in an undefined state after this, so exiting is the only safe option.
process.on('uncaughtException', (err) => {
  console.error('UNCAUGHT EXCEPTION! Shutting down...');
  console.error(err.name, err.message);
  console.error(err.stack);
  process.exit(1); // Let your process manager (PM2, Docker) restart the app
});

// Unhandled promise rejections -- fires when an async operation rejects
// and no .catch() or try/catch is waiting for it.
// Starting with Node.js 15+, unhandled rejections crash the process by default.
process.on('unhandledRejection', (reason, promise) => {
  console.error('UNHANDLED REJECTION! Shutting down...');
  console.error(reason);
  
  // Close server gracefully (finish handling in-flight requests), then exit
  server.close(() => {
    process.exit(1);
  });
});

// SIGTERM handling -- sent by orchestrators (Heroku, Docker, Kubernetes)
// when they want your process to stop. You get a brief window to finish
// current work before the process is forcefully killed (SIGKILL).
process.on('SIGTERM', () => {
  console.log('SIGTERM received. Shutting down gracefully');
  server.close(() => {
    console.log('Process terminated');
  });
});
```

<Tip>
  **Production tip:** Always run your Node.js process behind a process manager like PM2 or inside a container orchestrator. When `process.exit(1)` fires, the manager automatically restarts the process, minimizing downtime. Without a process manager, your app just stays dead after a crash.
</Tip>

## Debugging Techniques

### Console Methods

```javascript theme={null}
// 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

```bash theme={null}
# 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

```json theme={null}
// .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

`console.log` is fine during development, but in production you need structured, level-based logging that can write to files, external services, or monitoring platforms. Winston is the most widely used logging library in the Node.js ecosystem. It separates the concept of *what* you log (levels and messages) from *where* it goes (transports like console, files, or cloud services).

```bash theme={null}
npm install winston
```

```javascript theme={null}
// 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

```javascript theme={null}
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

```javascript theme={null}
// 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
