Skip to main content

Express Error Logging

Error logging is a critical part of developing robust Express.js applications. By implementing proper logging, you can track errors, debug issues, and maintain application reliability. This guide will walk you through various approaches to logging errors in Express applications.

Introduction to Error Logging

Error logging provides visibility into application failures and helps developers identify and fix issues quickly. In production environments, where you don't want to display error details to users, logging becomes essential for troubleshooting problems behind the scenes.

Effective error logging should:

  • Capture error details (message, stack trace, timestamp)
  • Store logs in an accessible format
  • Provide context about the error (request parameters, user info)
  • Help with debugging without exposing sensitive information to end users

Basic Console Logging

The simplest form of error logging uses JavaScript's built-in console methods.

javascript
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});

When an error occurs, this middleware will print the error stack trace to the console:

Error: Database connection failed
at /app/routes/users.js:42:12
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async /app/app.js:24:5

While useful during development, console logging has limitations:

  • Logs disappear when the process restarts
  • Not suitable for production environments
  • Lacks structure and searching capabilities

Using Logging Libraries

For more robust logging, specialized libraries like Winston or Pino provide advanced features.

Winston Logger Example

First, install Winston:

bash
npm install winston

Then create a logger configuration:

javascript
const winston = require('winston');

// Create a logger
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' }),
new winston.transports.Console()
]
});

// Error middleware using Winston
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
method: req.method,
path: req.path,
ip: req.ip
});

res.status(500).send('Internal Server Error');
});

This creates log entries like:

json
{
"level": "error",
"message": "Database connection failed",
"stack": "Error: Database connection failed\n at /app/routes/users.js:42:12...",
"method": "GET",
"path": "/users/profile",
"ip": "192.168.1.1",
"timestamp": "2023-11-09T12:34:56.789Z"
}

Pino Logger Example

Pino is known for its high performance. Install it with:

bash
npm install pino pino-http

Implement it in your Express app:

javascript
const express = require('express');
const pino = require('pino');
const pinoHttp = require('pino-http');

const app = express();

// Create a logger instance
const logger = pino({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
});

// Add request logging middleware
const loggerMiddleware = pinoHttp({ logger });
app.use(loggerMiddleware);

// Error handling middleware
app.use((err, req, res, next) => {
req.log.error({ err, req, res }, 'An error occurred');
res.status(500).send('Server Error');
});

Custom Error Logging Middleware

You can create custom middleware that logs different types of errors differently:

javascript
function errorLogger(err, req, res, next) {
// Log information about the request
const requestInfo = {
method: req.method,
url: req.url,
headers: req.headers,
query: req.query,
body: req.body,
timestamp: new Date().toISOString()
};

// Determine error severity
if (err.status >= 500) {
console.error('SERVER ERROR:', err.message, requestInfo, err.stack);
} else if (err.status >= 400) {
console.warn('CLIENT ERROR:', err.message, requestInfo);
} else {
console.info('OTHER ERROR:', err.message, requestInfo);
}

next(err); // Pass error to the next error handler
}

// Register the middleware before your error handler
app.use(errorLogger);

app.use((err, req, res, next) => {
res.status(err.status || 500).send(err.publicMessage || 'Something went wrong');
});

Contextual Logging with Request Identifiers

Assign a unique ID to each request to track it through the system:

javascript
const { v4: uuidv4 } = require('uuid');
const winston = require('winston');

// Create logger
const logger = winston.createLogger(/* configuration */);

// Request ID middleware
app.use((req, res, next) => {
req.requestId = uuidv4();
res.setHeader('X-Request-ID', req.requestId);
next();
});

// Error logging middleware
app.use((err, req, res, next) => {
logger.error({
requestId: req.requestId,
message: err.message,
stack: err.stack,
// Other relevant information
});

next(err);
});

Logging to External Services

For production applications, consider sending logs to external services for better monitoring, alerting, and analysis.

Example with Morgan and Winston to Log to Files and Elasticsearch

javascript
const express = require('express');
const winston = require('winston');
const morgan = require('morgan');
const { ElasticsearchTransport } = require('winston-elasticsearch');

const app = express();

// Create Winston logger with multiple transports
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new ElasticsearchTransport({
level: 'error',
clientOpts: { node: 'http://localhost:9200' },
index: 'app-logs'
})
]
});

// Use Morgan for HTTP request logging, writing through Winston
app.use(morgan('combined', {
stream: {
write: (message) => logger.info(message.trim())
}
}));

// Error handler that uses the logger
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
method: req.method,
url: req.originalUrl,
user: req.user ? req.user.id : 'anonymous',
body: req.body,
query: req.query,
params: req.params
});

res.status(500).json({
error: 'Internal Server Error',
requestId: req.requestId
});
});

Best Practices for Express Error Logging

  1. Log Levels: Use appropriate log levels (debug, info, warning, error) to categorize messages.
  2. Structured Logging: Log in structured formats like JSON for easier parsing and analysis.
  3. Include Context: Log relevant request data (URL, method, user info) to help with debugging.
  4. Sanitize Sensitive Data: Remove sensitive information like passwords and tokens before logging.
  5. Centralized Logging: In distributed systems, send logs to a central location.
  6. Request IDs: Include unique identifiers to track requests across services.
  7. Log Rotation: Implement log rotation to prevent log files from growing too large.

Real-world Example: Complete Logging Setup

Here's a comprehensive example that incorporates many of the best practices:

javascript
const express = require('express');
const winston = require('winston');
const expressWinston = require('express-winston');
const { v4: uuidv4 } = require('uuid');

const app = express();

// Body parsing middleware
app.use(express.json());

// Add request ID middleware
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
});

// Request logging middleware
app.use(expressWinston.logger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'requests.log' })
],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
meta: true,
msg: "HTTP {{req.method}} {{req.url}}",
expressFormat: true,
colorize: false,
dynamicMeta: (req, res) => {
return {
requestId: req.id,
userId: req.user ? req.user.id : null
};
}
}));

// Application routes
app.get('/api/users', (req, res) => {
// Route handler code...
});

// Error logging middleware
app.use(expressWinston.errorLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'errors.log' })
],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
dumpExceptions: true,
showStack: true,
dynamicMeta: (req, res, err) => {
return {
requestId: req.id,
userId: req.user ? req.user.id : null
};
}
}));

// Error handling middleware
app.use((err, req, res, next) => {
// Send appropriate response to client
res.status(err.status || 500).json({
error: {
message: process.env.NODE_ENV === 'production'
? 'Internal Server Error'
: err.message,
requestId: req.id
}
});
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Summary

Effective error logging is essential for maintaining and troubleshooting Express applications. By implementing proper logging strategies, you can:

  • Quickly identify and fix issues in development and production
  • Track errors across your application
  • Provide better support to users experiencing problems
  • Improve application reliability over time

Remember that logging is a balance: log enough information to be useful for debugging, but be mindful of performance impacts and security concerns with sensitive data.

Additional Resources

Exercises

  1. Set up a basic Express application with Winston logging that writes to both console and file.
  2. Extend your logging implementation to include request IDs and user information when available.
  3. Create a custom error logger that categorizes errors by type and logs them differently.
  4. Implement log rotation for your application logs.
  5. Add Elasticsearch logging to your application and create a simple dashboard to view errors.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)