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
7.1 KiB
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));
});
});