Question
How should errors be effectively logged in a production Node.js Express application?
Asked by: USER7117
84 Viewed
84 Answers
Answer (84)
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);
});
});
```