Files
APAW/.kilo/skills/nodejs-error-handling/SKILL.md
¨NW¨ 8fcd8f8a9b feat: add comprehensive NodeJS development skills and rules
Based on Planner and Memory Manager analysis:

New Skills (8):
- nodejs-express-patterns: App structure, routing, middleware
- nodejs-security-owasp: OWASP Top 10 security practices
- nodejs-testing-jest: Unit/integration tests, mocking
- nodejs-auth-jwt: JWT authentication, OAuth, sessions
- nodejs-error-handling: Error classes, middleware, async handlers
- nodejs-middleware-patterns: Auth, validation, rate limiting
- nodejs-db-patterns: SQLite, PostgreSQL, MongoDB patterns
- nodejs-npm-management: package.json, scripts, dependencies

New Rules:
- nodejs.md: Code style, security, best practices

Updated:
- backend-developer.md: Added skills reference table

Milestone: #48 NodeJS Development Coverage
Related: Planner & Memory Manager analysis results
2026-04-05 02:39:06 +01:00

7.1 KiB

NodeJS Error Handling

Comprehensive error handling patterns for Node.js applications.

Error Classes

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

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

class ValidationError extends AppError {
  constructor(errors) {
    super('Validation failed', 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);
  }
}

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

Error Middleware

// src/middleware/error.js
const AppError = require('../utils/AppError');

// Not found handler
function notFoundHandler(req, res, next) {
  next(new AppError(`Cannot find ${req.originalUrl} on this server`, 404));
}

// Global error handler
function errorHandler(err, req, res, next) {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';
  
  if (process.env.NODE_ENV === 'development') {
    sendErrorDev(err, res);
  } else {
    sendErrorProd(err, res);
  }
}

function sendErrorDev(err, res) {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
    stack: err.stack,
    error: err
  });
}

function sendErrorProd(err, res) {
  // Operational error: send message to client
  if (err.isOperational) {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message
    });
  } else {
    // Programming error: don't leak details
    console.error('ERROR:', err);
    res.status(500).json({
      status: 'error',
      message: 'Something went wrong'
    });
  }
}

module.exports = { notFoundHandler, errorHandler };

Async Error Wrapper

// src/middleware/asyncHandler.js
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Usage
router.get('/users/:id', asyncHandler(controller.getById));

// Or wrap entire controller
const wrapController = (controller) => {
  const wrapped = {};
  for (const [key, fn] of Object.entries(controller)) {
    wrapped[key] = asyncHandler(fn.bind(controller));
  }
  return wrapped;
};

module.exports = { asyncHandler, wrapController };

Validation Errors

// src/middleware/validation.js
const { validationResult } = require('express-validator');
const { ValidationError } = require('../utils/AppError');

function validate(validations) {
  return async (req, res, next) => {
    await Promise.all(validations.map(validation => validation.run(req)));
    
    const errors = validationResult(req);
    
    if (errors.isEmpty()) {
      return next();
    }
    
    const extractedErrors = errors.array().map(err => ({
      field: err.path,
      message: err.msg,
      value: err.value
    }));
    
    return next(new ValidationError(extractedErrors));
  };
}

module.exports = { validate };

Database Error Handling

// src/middleware/dbErrorHandler.js
const AppError = require('../utils/AppError');

function dbErrorHandler(err, req, res, next) {
  // PostgreSQL unique violation
  if (err.code === '23505') {
    const field = err.constraint.split('_')[0];
    return next(new AppError(`${field} already exists`, 409));
  }
  
  // PostgreSQL foreign key violation
  if (err.code === '23503') {
    return next(new AppError('Referenced resource not found', 400));
  }
  
  // SQLite unique violation
  if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
    return next(new AppError('Resource already exists', 409));
  }
  
  // MongoDB duplicate key
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    return next(new AppError(`${field} already exists`, 409));
  }
  
  // MongoDB validation error
  if (err.name === 'ValidationError') {
    const messages = Object.values(err.errors).map(e => e.message);
    return next(new AppError(messages.join('. '), 400));
  }
  
  // MongoDB cast error
  if (err.name === 'CastError') {
    return next(new AppError('Invalid ID format', 400));
  }
  
  next(err);
}

module.exports = dbErrorHandler;

Logging Errors

// src/utils/logger.js
const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: process.env.NODE_ENV === 'development' 
    ? { target: 'pino-pretty' }
    : undefined
});

function logError(err, req = null) {
  logger.error({
    message: err.message,
    stack: err.stack,
    statusCode: err.statusCode || 500,
    path: req?.path,
    method: req?.method,
    user: req?.user?.id,
    body: req?.body,
    query: req?.query,
    params: req?.params,
    timestamp: new Date().toISOString()
  });
}

function logInfo(message, data = {}) {
  logger.info({ message, ...data });
}

module.exports = { logger, logError, logInfo };

Unhandled Rejections

// src/server.js
process.on('unhandledRejection', (err) => {
  console.error('UNHANDLED REJECTION:', err);
  logError(err);
  
  // Graceful shutdown
  server.close(() => {
    process.exit(1);
  });
});

process.on('uncaughtException', (err) => {
  console.error('UNCAUGHT EXCEPTION:', err);
  logError(err);
  
  // Must exit immediately for uncaught exceptions
  process.exit(1);
});

API Error Responses

// Standardized error responses
const errorResponses = {
  400: {
    status: 'fail',
    message: 'Bad Request',
    errors: []
  },
  401: {
    status: 'fail',
    message: 'Unauthorized'
  },
  403: {
    status: 'fail',
    message: 'Forbidden'
  },
  404: {
    status: 'fail',
    message: 'Not Found'
  },
  409: {
    status: 'fail',
    message: 'Conflict'
  },
  500: {
    status: 'error',
    message: 'Internal Server Error'
  }
};

// Example responses
{
  "status": "fail",
  "message": "Validation failed",
  "errors": [
    { "field": "email", "message": "Invalid email format" },
    { "field": "password", "message": "Must be at least 8 characters" }
  ]
}

Error Handling in Tests

// tests/unit/errors.test.js
describe('Error Handling', () => {
  it('should handle AppError', () => {
    const error = new AppError('Test error', 400);
    
    expect(error.statusCode).toBe(400);
    expect(error.status).toBe('fail');
    expect(error.isOperational).toBe(true);
  });
  
  it('should handle async errors', async () => {
    const req = {};
    const res = {};
    const next = jest.fn();
    
    const handler = asyncHandler(async () => {
      throw new AppError('Test', 400);
    });
    
    await handler(req, res, next);
    
    expect(next).toHaveBeenCalledWith(expect.any(AppError));
  });
});