In Node.js development, keeping your application fast and reliable is non-negotiable. Logging is one of the most dependable ways to see what’s going on inside your app, catch issues early, and tune performance over time.
Winston is a go-to logging library for many Node.js developers, offering flexibility, structured output, and features that perform well in production environments.
In this blog, we’ll walk through how to set it up, tailor it for different environments, and use advanced options — with examples you can plug straight into your project.
What is Winston?
Winston is a logging library for Node.js that supports multiple outputs, called “transports.” Logs can go to the console, a file, or a remote endpoint — all without changing your core logic.
You can control the format, set different log levels, and use more than one transport at the same time. This makes it useful for anything from debugging a small script to handling logs in production services.
Why Use Winston for Logging in Node.js?
Logging helps you debug, monitor, and keep your application stable. Winston strikes a balance between flexibility and straightforward setup, making it a go-to for many Node.js teams.
Multiple Transports
Send logs to the console, files, or external services—all with one configuration. You can also build custom transports to fit your own logging pipelines.
Log Levels
Built-in levels like info
, debug
, warn
, and error
let you filter and prioritize messages based on severity.
Custom Formats
Choose JSON for structured logs, plain text for clarity, or define your own formats to align with tools and internal standards.
Configuration
Just a few lines set up levels, formats, and transports—easy to expand as your needs evolve.
Performance
Winston delivers efficient logging performance in production environments. Based on industry benchmarks:
- Filesystem logging: 0.0005ms per log event (0.0005% of typical 100ms response time)
- HTTP throughput: ~3,400 requests/second with standard configuration
- Memory overhead: Approximately 50% performance reduction with active logging vs no logging
- Event loop impact: Unblocks faster than competing libraries in multi-transport scenarios
These numbers vary significantly based on transport configuration, log volume, and system resources.
Getting Started with Winston in Node.js
Winston can be added to a project in minutes. The setup process has two main steps: installing the package and creating a logger. From there, you can adjust it to match your logging needs.
1. Install Winston
Use your preferred package manager:
npm install winston
# or
yarn add winston
This installs the core Winston library, which includes built-in transports for console and file logging. Additional transports for services like HTTP, Syslog, or cloud platforms can be added separately.
2. Create a Basic Logger
Start by importing Winston and defining a logger instance:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info', // logs this level and above
format: winston.format.simple(), // plain text output
transports: [
new winston.transports.Console(), // output to terminal
],
});
// Example log messages
logger.info('Application started');
logger.warn('Cache is nearing capacity');
logger.error('Failed to connect to database');
How it works:
level
controls the minimum severity of messages to log. In this example, messages atinfo
,warn
, anderror
levels will be recorded.format
defines how logs are displayed. Here we’re using thesimple
format, but JSON and custom formats are also supported.transports
define where logs go. You can add more than one transport to send logs to multiple destinations at once.
3. Adjust Log Levels
Winston follows the npm log level convention:
Level | Purpose | Example Usage |
---|---|---|
error | Critical issues, service failures | Database outage, failed API calls |
warn | Potential problems | Deprecated API usage |
info | General operational messages | Server started, user login |
http | HTTP request logging | GET /api/products 200 |
verbose | Detailed app behavior | Internal workflow details |
debug | Development/debugging information | Variable values, function calls |
silly | Lowest priority, high-volume logs | Extremely granular debugging |
By default, level: 'info'
logs info
and anything more severe. Setting it to debug
will also include debug
, verbose
, and silly
logs.
Advanced Configuration Options
Winston’s flexibility makes it useful beyond basic setups. These configurations help when running in production or handling large volumes of logs.
1. Adding Multiple Transports
Send logs to more than one destination at once — for example, the console for live monitoring and a file for historical analysis:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/app.log' }),
],
});
logger.info('Logged to both console and file');
2. Customizing Log Formats
The winston.format
module lets you control how logs look. Use built-in formats like json()
or define your own.
const winston = require('winston');
const { printf, combine, timestamp, errors } = winston.format;
const customFormat = printf(({ timestamp, level, message, stack }) => {
return `${timestamp} [${level}]: ${stack || message}`;
});
const logger = winston.createLogger({
level: 'debug',
format: combine(
timestamp(),
errors({ stack: true }),
customFormat
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/app.log' }),
],
});
logger.error(new Error('Something went wrong'));
3. Setting Log Levels
Levels define which logs are recorded. They follow a hierarchy — for example, warn
logs warnings and errors but ignores lower levels.
const winston = require('winston');
const logger = winston.createLogger({
level: 'warn',
transports: [new winston.transports.Console()],
});
logger.info('Ignored');
logger.warn('This is a warning');
logger.error('This is an error');
4. Asynchronous Transports for Performance
When logging to remote services, async transports can prevent blocking the main thread.
const { createLogger, transports } = require('winston');
const logger = createLogger({
transports: [
new transports.Http({
host: 'log.example.com',
path: '/logs',
ssl: true,
}),
],
});
Async transports batch log events and send them in the background, reducing latency for high-throughput apps.
5. Log Rotation
Large log files can slow down analysis and fill storage. Use a rotation transport to split logs by size or date.
npm install winston-daily-rotate-file
const DailyRotateFile = require('winston-daily-rotate-file');
const logger = winston.createLogger({
transports: [
new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
6. Environment-Based Configuration
You can adjust logging settings depending on the environment:
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
transports: [new winston.transports.Console()],
});
This ensures detailed logs in development without overloading production systems.
7. Production Performance Tuning
High-volume logging can consume CPU, memory, and storage quickly. In production, tuning Winston’s configuration can help keep logging efficient without losing important events.
Memory Management for High Log Volumes
- Reduce verbosity: In production, use a higher minimum log level (
warn
orerror
) so low-priority messages don’t consume resources. - Limit file size and count: Use
maxsize
andmaxFiles
to prevent unbounded log growth. - Enable
tailable
: Ensures logs overwrite the oldest file instead of holding everything in memory.
const winston = require('winston');
const logger = winston.createLogger({
level: 'warn', // Only log warnings and errors
transports: [
new winston.transports.File({
filename: 'app.log',
maxsize: 10 * 1024 * 1024, // 10 MB
maxFiles: 5, // Keep last 5 log files
tailable: true // Overwrite oldest file first
})
]
});
Log Sampling for High-Traffic Endpoints
For endpoints that generate thousands of logs per second, sampling reduces noise and storage use without removing all visibility.
// Log ~10% of requests
const shouldLog = () => Math.random() < 0.1;
if (shouldLog()) {
logger.info('High-frequency operation completed');
}
This approach is useful for repetitive success events where full detail isn’t needed.
Handling Log Volume Spikes
Unexpected spikes (e.g., incident storms) can overwhelm logging systems. To handle them:
- Circuit breakers — Temporarily disable non-critical logs during load spikes to protect system performance.
- Async transports — Use transports that buffer and send logs in the background so the event loop isn’t blocked. This is especially important for network-based logging.
- Dynamic log levels — Lower verbosity automatically when CPU/memory is high:
if (systemLoadHigh()) {
logger.level = 'error'; // Only critical logs
}
Centralize Winston Logs with Popular Services and Tools
As your application scales and your log data grows, managing logs across multiple servers and instances can become a challenge. Centralizing log management helps you aggregate, search, analyze, and monitor your logs from a single location, making troubleshooting and performance monitoring much more efficient.
Here are some popular options for centralizing your Winston logs, including sending logs to cloud services or log management tools.
1. Cloud Log Management Services
Cloud-based logging services offer scalability, reliability, and powerful analytics features. These services can handle large volumes of logs, allow for fast searching, and provide powerful dashboards for monitoring.
AWS CloudWatch Logs
If you’re already using AWS for your infrastructure, CloudWatch Logs is a great choice for centralizing your logs. You can configure Winston to send logs to CloudWatch, where they can be stored, analyzed, and visualized.
Setup Example:
const winston = require('winston');
const CloudWatchTransport = require('winston-cloudwatch');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new CloudWatchTransport({
logGroupName: 'my-log-group',
logStreamName: 'my-log-stream',
awsRegion: 'us-east-1',
}),
],
});
logger.info('This log will be sent to AWS CloudWatch');
Google Cloud Logging
Google Cloud offers a similar service called Stackdriver Logging (now part of Google Cloud Operations). It integrates well with other Google Cloud services and can help with analyzing logs in real-time.
Setup Example:
const { Logging } = require('@google-cloud/logging');
const winston = require('winston');
const logging = new Logging();
const log = logging.log('my-log');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new winston.transports.Http({
host: 'logging.googleapis.com',
path: `/v2/${log.name}/entries:write`,
}),
],
});
logger.info('This log will be sent to Google Cloud Logging');
Azure Monitor
If your infrastructure runs on Microsoft Azure, you can use Azure Monitor to collect and analyze logs. Azure Monitor integrates with Application Insights to provide performance metrics and diagnostics from your applications.
Setup Example:
const winston = require('winston');
const AzureLog = require('winston-azure-logger');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new AzureLog({
instrumentationKey: 'your-instrumentation-key',
}),
],
});
logger.info('This log will be sent to Azure Monitor');
2. Log Management Tools
Log management tools provide a dedicated platform to ingest, analyze, and visualize logs. These tools usually come with advanced querying capabilities, alerting features, and integration options for other parts of your infrastructure.
Last9
Last9 is a telemetry platform that brings metrics, logs, and traces together in one place. It’s built for developers, SREs, and AI agents who need unified observability across distributed systems and microservices.
If you’re using Winston in a Node.js app, you can route those logs to Last9 through the OpenTelemetry Collector.
Option A — Recommended: File → OTel Collector → Last9
1) Winston: write structured JSON to a file
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(), // structured logs
transports: [
new winston.transports.File({ filename: 'logs/app.log' }),
],
});
// example usage
logger.info('user logged in', { userId: '123', route: '/login' });
logger.error('db connection failed', { retry: false });
2) OpenTelemetry Collector: tail the file and export to Last9 (OTLP)
Save as otel-collector.yaml
:
receivers:
filelog:
include:
- /absolute/path/to/your/project/logs/app.log
start_at: beginning
operators:
- type: json_parser
parse_from: body
# (optional) map JSON fields into attributes
# On errors, body remains a string.
exporters:
otlphttp:
# Use the OTLP endpoint shown in your Last9 OpenTelemetry Integration page
# (it will look like an OTLP /v1/logs endpoint).
endpoint: https://<your-last9-otlp-endpoint>
headers:
X-LAST9-API-TOKEN: "<YOUR_LAST9_API_TOKEN>"
processors:
batch: {}
service:
pipelines:
logs:
receivers: [filelog]
processors: [batch]
exporters: [otlphttp]
Run the collector:
otelcol --config otel-collector.yaml
- Last9 states it’s OpenTelemetry-compatible (so OTLP export is supported).
- Public APIs (including ingestion-backed endpoints) use the
X-LAST9-API-TOKEN
header for auth.
3) Verify in Last9
Use the Logs UI / LogQL-compatible Explorer to query what you just sent.
Option B — Custom HTTP transport (only if you already have a JSON gateway)
If you already expose an internal HTTP gateway that accepts JSON logs and forwards them to the Collector, you can post to it from Winston:
npm install winston-transport node-fetch
const winston = require('winston');
const Transport = require('winston-transport');
const fetch = require('node-fetch');
class HttpJsonTransport extends Transport {
constructor(opts) {
super(opts);
this.url = opts.url; // e.g., http://localhost:3000/logs
}
async log(info, callback) {
setImmediate(() => this.emit('logged', info));
try {
await fetch(this.url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(info),
});
} catch (_) {
// swallow or add retry/backoff as needed
}
callback();
}
}
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new HttpJsonTransport({ url: 'http://localhost:3000/logs' }),
],
});
Your gateway (or the Collector’s otlphttp
/filelog
path) should forward to Last9’s OTLP endpoint with the X-LAST9-API-TOKEN
header.
Papertrail
Papertrail is another cloud-based log management service that offers easy log aggregation and real-time log monitoring. It provides advanced filtering and search capabilities, making it easy to track down issues across distributed systems.
Setup Example:
const winston = require('winston');
const Papertrail = require('winston-papertrail').Papertrail;
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new Papertrail({
host: 'logs.papertrailapp.com',
port: 12345,
program: 'my-app',
}),
],
});
logger.info('This log will be sent to Papertrail');
This example assumes there's a custom transport winston-last9
for integrating Winston with Last9, similar to the Loggly transport in your original example. If such a transport doesn't exist, you could build one or use Last9's REST API to send logs via HTTP.
Replace the placeholders with your actual API key and project details.
ELK Stack (Elasticsearch, Logstash, Kibana)
The ELK Stack is a powerful open-source solution for aggregating, indexing, and visualizing logs. You can send your Winston logs to Elasticsearch via Logstash, and then use Kibana for real-time analysis and dashboards.
Setup Example:
const winston = require('winston');
const Elasticsearch = require('winston-elasticsearch');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new Elasticsearch({
level: 'info',
clientOpts: {
node: 'http://localhost:9200',
},
}),
],
});
logger.info('This log will be sent to Elasticsearch');
3. Self-Hosted Solutions
If you prefer to manage your own log aggregation system, you can set up your own logging infrastructure using open-source tools like Fluentd or Logstash.
Fluentd
Fluentd is an open-source data collector for unified logging. It can be used to collect logs from multiple sources, transform the data, and send it to various destinations like Elasticsearch, Kafka, or databases.
Setup Example:
const winston = require('winston');
const Fluentd = require('fluent-logger').createFluentSender;
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new Fluentd('my-fluentd-host', 24224),
],
});
logger.info('This log will be sent to Fluentd');
Logstash
Logstash is a powerful tool for log aggregation, filtering, and forwarding. You can configure Logstash to receive logs from your application, process them, and then forward them to Elasticsearch or other log storage systems.
Setup Example: Sending logs to Logstash can be done by using TCP or UDP transports in combination with the winston-logstash package.
const winston = require('winston');
const Logstash = require('winston-logstash');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console(),
new Logstash({
host: 'logstash-server', // Replace with your Logstash server
port: 5044, // Default port for Logstash
}),
],
});
logger.info('This log will be sent to Logstash');
Make sure you have Logstash running and configured to listen on the specified port. This code will send Winston logs to Logstash, which can then forward them to other destinations like Elasticsearch for further processing.

Configure Transports in Winston
In Winston, a transport decides where your logs go — console, file, remote endpoint, or even a custom service. You can use multiple transports at the same time, each with its own configuration and log level.
1. Console Transport
Best suited for local development and debugging. The console transport can display logs in plain text or colorized output for quick scanning.
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
],
});
logger.info('This is an info message.');
logger.error('This is an error message.');
2. File Transport
For persistent storage, file transports record logs in a specified file. This is common in production, where logs need to be archived or processed later.
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.File({ filename: 'logs/app.log' })
],
});
logger.info('This log will be written to a file.');
3. File Transport with Log Rotation
Large log files can slow down analysis and use unnecessary disk space. The winston-daily-rotate-file
transport rotates logs by date or size and automatically deletes older files.
npm install winston-daily-rotate-file
const winston = require('winston');
require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
})
],
});
logger.info('Rotated log file created.');
4. HTTP Transport
Sends logs to a remote endpoint over HTTP/HTTPS. Useful for centralized logging platforms like Loggly or self-hosted log collectors.
npm install winston-transport
const winston = require('winston');
const Transport = require('winston-transport');
class HttpTransport extends Transport {
log(info, callback) {
// Send log to a remote HTTP endpoint here
setImmediate(() => this.emit('logged', info));
callback();
}
}
const logger = winston.createLogger({
transports: [new HttpTransport()],
});
logger.info('This log will be sent to a remote server.');
5. Custom Transports
You can create a transport for any destination — a database, message queue, or external service. To do this, extend winston.Transport
(not transportStreamOptions
, which is incorrect).
const winston = require('winston');
const Transport = require('winston-transport');
class MyCustomTransport extends Transport {
log(info, callback) {
setImmediate(() => this.emit('logged', info));
console.log('Custom log output:', info.message);
callback();
}
}
const logger = winston.createLogger({
transports: [new MyCustomTransport()],
});
logger.info('Custom transport log.');
6. Log Levels per Transport
Each transport can have its own log level, letting you send detailed logs to one destination and only critical logs to another.
const winston = require('winston');
const logger = winston.createLogger({
transports: [
new winston.transports.Console({ level: 'warn' }),
new winston.transports.File({ filename: 'logs/app.log', level: 'info' }),
],
});
logger.info('Only written to file.');
logger.warn('Written to file and console.');
logger.error('Written to file and console.');
Format Log Messages in Winston
Winston gives you the flexibility to customize your log messages, whether you want to add timestamps, log levels, custom formats, colors, or even extra contextual details.
In this section, we'll take a look at how to format your log messages in Winston using various options to make them more structured and easier to read.
1. Basic Log Format
The simplest way to format log messages is by using the basic format, which outputs only the log message without any additional context. This can be helpful for quick debugging when you don’t need much information.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.simple(), // Simple format: just log message
}),
],
});
logger.info('This is a simple log message');
Output:
This is a simple log message
2. Adding Timestamps
Adding timestamps to your logs is pretty common, as it helps you track when each event happened. Winston has a built-in timestamp
format that automatically adds a timestamp to each log message.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple() // Combine timestamp with simple log message
),
}),
],
});
logger.info('This log includes a timestamp');
Output:
2025-01-02T10:00:00.000Z This log includes a timestamp
3. Colorizing Logs
Colorizing logs is especially useful in a console environment to make different log levels (like info, warn, and error) stand out. Winston has a colorize
format that colorizes the log level, helping you quickly identify the severity of each log.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(), // Adds color to log levels
winston.format.simple()
),
}),
],
});
logger.info('This log is colorized');
logger.warn('This is a warning message');
logger.error('An error has occurred');
Output:
info: This log is colorized
warn: This is a warning message
error: An error has occurred
4. Custom Log Formats
Winston allows you to define your own custom formats by combining different formatting methods. The combine
method lets you stack multiple formats, such as adding timestamps, colors, and custom message structures.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(), // Add timestamp to each log
winston.format.printf(({ timestamp, level, message }) => {
return `${timestamp} [${level}]: ${message}`; // Custom log format
})
),
}),
],
});
logger.info('This is a custom formatted log');
Output:
2025-01-02T10:00:00.000Z [info]: This is a custom formatted log
5. JSON Log Format
JSON formatting is great for structured logging, especially when you need to parse and analyze logs later (e.g., for log management or cloud services). Winston supports the json
format, which outputs logs in a machine-readable format.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.json() // Format log messages as JSON
),
}),
],
});
logger.info('This log is in JSON format');
Output:
{
"message": "This log is in JSON format",
"level": "info",
"timestamp": "2025-01-02T10:00:00.000Z"
}
6. Combine Formats for Better Structure
You can stack multiple formats to create more detailed, structured logs that include timestamps, log levels, and even additional user-related fields. For example, you might want to track which user-generated the log.
Example:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(({ timestamp, level, message, user }) => {
return `${timestamp} [${level}] ${user ? `[User: ${user}]` : ''}: ${message}`;
})
),
}),
],
});
logger.info('This is a log with additional user information', { user: 'john_doe' });
Output:
2025-01-02T10:00:00.000Z [info] [User: john_doe]: This is a log with additional user information
7. Log Rotation and File Formatting
For logs written to files, it’s important to format them in a way that makes them easier to read and analyze later. You can combine timestamp, JSON, or custom formats to ensure that logs are well-structured in the file system.
Example:
const winston = require('winston');
require('winston-daily-rotate-file');
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json() // Log as JSON for structured logging
),
}),
],
});
logger.info('This log is written to a rotating log file');
This will create daily log files in JSON format, making it easier to process them later.
Logging Errors in Winston
In Winston, there are several methods for logging errors, including handling uncaught exceptions and unhandled promise rejections. These ensure that your application can log even the most unexpected issues that occur during runtime.
Let’s explore how you can configure Winston to handle and log these error scenarios effectively.
1. Logging Errors with the Default Transport
Winston has built-in support for logging errors through its transport system. By default, you can use any of the transports (such as the console or file transport) to log error messages that you manually capture or throw.
const logger = winston.createLogger({
level: 'info', // Default log level
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
],
});
logger.error('This is an error message.');
In this example, an error message is logged to the console. You can configure Winston to log errors to a file or remote service as needed.
2. Handling Uncaught Exceptions
Uncaught exceptions are errors that occur in the application but aren’t caught by any try-catch block. These errors typically crash the application if not handled properly.
Winston allows you to log these exceptions and prevent the application from crashing immediately, and provides a built-in option for catching uncaught exceptions and logging them.
const logger = winston.createLogger({
level: 'info',
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
new winston.transports.File({ filename: 'logs/uncaught_exceptions.log' }),
],
exceptionHandlers: [
new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.simple()) }),
new winston.transports.File({ filename: 'logs/uncaught_exceptions.log' }),
],
});
throw new Error('This is an uncaught exception'); // This will be logged
- The
exceptionHandlers
property ensures that Winston catches and logs uncaught exceptions. - Errors are logged both to the console and to a file (
uncaught_exceptions.log
). - The application won’t crash immediately after an uncaught exception, allowing for better control and monitoring.
3. Handling Unhandled Promise Rejections
Promise rejections occur when a promise is rejected, but no handler is attached (i.e., no .catch()
or .then()
to handle the rejection.
By default, unhandled promise rejections in Node.js can cause the application to crash, but it’s better to log them before crashing or terminating the process. Winston also has a built-in way to handle unhandled promise rejections.
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
Promise.reject(new Error('This is an unhandled promise rejection')); // This will be logged
- The
process.on('unhandledRejection')
event handler is used to catch unhandled promise rejections. - The rejection is logged using Winston, and the message is written to both the console and a log file.
This way, you ensure that no rejection goes unnoticed, even if it wasn’t properly caught or handled in the code.
4. Identifying and Logging Errors in Async Functions
In asynchronous code, it’s common to encounter errors that may not be immediately apparent because they are part of a promise chain or async function.
If you want to ensure that errors in async functions are logged, you can use try-catch
blocks combined with Winston’s error logging.
async function someAsyncFunction() {
try {
throw new Error('This is an error in an async function');
} catch (error) {
logger.error('Caught an error:', error.message);
}
}
Here, an error within an async function is caught by the catch
block, and the error message is logged using Winston. This ensures that errors in asynchronous code are handled and logged consistently.
5. Custom Error Handling Middleware (For Web Servers)
If you’re using Winston in a web application (e.g., with Express), it’s a good practice to create custom error-handling middleware that logs errors from routes or middleware.
const app = express();
app.get('/error', (req, res) => {
throw new Error('Something went wrong!');
});
// Custom error handling middleware
app.use((err, req, res, next) => {
logger.error(`Error occurred: ${err.message}`);
res.status(500).send('Something went wrong!');
});
In this example, if an error is thrown in any route, the custom error-handling middleware logs the error using Winston and sends a generic response to the client.
Best Practices for Winston Logging in Node.js
While Winston offers flexibility, here are some best practices to follow for effective logging:
- Use Different Log Levels: Don’t log everything as
info
orerror
. Use a combination ofdebug
,warn
, anderror
to provide more meaningful insights, making it easier to filter logs by severity. - Log Contextually: Include context in your logs, such as user IDs, request IDs, or other relevant details. This extra context can help with troubleshooting.
- Avoid Overusing Debug Logs in Production: While
debug
logs are great for development, they can add unnecessary overhead in production. Adjust your log level in production environments to avoid clutter. - Log Error Stack Traces: Always log error stack traces, especially in asynchronous code. Stack traces can help you quickly pinpoint the root cause of issues.
- Rotate Log Files: Log files can grow large as your application scales. Use
winston-daily-rotate-file
to automatically rotate log files and prevent them from getting too big. - Monitor Logging Performance: Track memory usage and logging latency. Set alerts for when logging operations exceed acceptable thresholds (>10ms per operation).
- Implement Security Sanitization: Always scrub sensitive data (passwords, tokens, PII) before logging. Use custom formatters to automatically remove or mask sensitive fields.
- Plan for Log Volume Scaling: Implement log sampling, use appropriate log levels for different environments, and set up log rotation with size limits to prevent disk space issues.
- Cost Management: For cloud logging, sample non-critical logs, use local buffering, and implement tiered logging strategies where only critical logs go to expensive destinations.
Conclusion
Winston gives you fine-grained control over logging in Node.js — choosing where logs go, how they’re formatted, and which levels to record. It works well for capturing everything from API errors to background job activity.
But logs alone can be hard to connect to the bigger picture. This is where Last9 helps. By sending Winston logs to Last9, you can:
- View logs alongside metrics and traces from the same service.
- Correlate an error log with the exact request trace that caused it.
- Set alerts that trigger when certain log patterns appear.
- Retain searchable logs for as long as your compliance or debugging needs require.
Instead of digging through separate systems, you get a single place to monitor application health and investigate issues end-to-end.
Get started with Last9 for free today!