How should errors be effectively logged in a production Node.js Express application?

Question

Grade: Education Subject: Support
How should errors be effectively logged in a production Node.js Express application?
Asked by:
84 Viewed 84 Answers

Answer (84)

Best Answer
(3452)
Effective error logging is crucial for monitoring application health, debugging issues, and ensuring stability in production. Best practices include: 1. **Use a Dedicated Logging Library:** Libraries like `Winston` or `Pino` are superior to `console.log` because they offer: * **Log Levels:** (e.g., `error`, `warn`, `info`, `debug`) to filter and prioritize messages. * **Transports:** Send logs to various destinations (console, file, database, remote services). * **Formatting:** Structure logs (e.g., JSON) for easier machine parsing and analysis. * **Contextual Data:** Easily add request details, user IDs, or other relevant information. 2. **Log Comprehensive Details:** For each error, log: * **Timestamp:** When the error occurred. * **Error Message:** `err.message`. * **Stack Trace:** `err.stack` (essential for debugging origin). * **Request Details:** `req.method`, `req.originalUrl`, `req.ip`, `req.headers` (especially `User-Agent`), `req.body` (carefully, avoid sensitive data). * **User Information:** If available (e.g., `req.user.id`). * **Error Type/Code:** `err.statusCode` or custom error codes. 3. **Distinguish Between Operational and Programming Errors:** Log operational errors (e.g., user input errors, 404s) at a `warn` or `info` level, and programming errors (bugs, unhandled exceptions) at an `error` or `fatal` level, as they indicate more severe issues. 4. **Integrate with External Monitoring Services:** Tools like Sentry, LogRocket, New Relic, or DataDog can aggregate logs, provide alerts, and offer advanced error tracking features. 5. **Handle Uncaught Exceptions and Unhandled Rejections:** These are critical and must be logged immediately before attempting a graceful shutdown. Example (using Winston): ```javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'error.log', level: 'error' }), // Add more transports like a remote log service in production ], }); // In your global error handling middleware: app.use((err, req, res, next) => { const statusCode = err.statusCode || 500; // Log programming errors at 'error' level if (!err.isOperational) { logger.error('Unhandled Programming Error:', { message: err.message, stack: err.stack, method: req.method, url: req.originalUrl, ip: req.ip, // ... other relevant request details }); } else { // Log operational errors at 'warn' or 'info' level logger.warn('Operational Error:', { message: err.message, statusCode: statusCode, method: req.method, url: req.originalUrl, }); } res.status(statusCode).json({ /* ... error response ... */ }); }); // Global unhandled promise rejection handler process.on('unhandledRejection', (reason, promise) => { logger.error('UNHANDLED REJECTION! 💥 Shutting down...', { reason: reason, promise: promise, }); // Optionally, gracefully shut down the server server.close(() => { process.exit(1); }); }); // Global uncaught exception handler process.on('uncaughtException', err => { logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...', { message: err.message, stack: err.stack, }); server.close(() => { process.exit(1); }); }); ```