Last9

Production Winston Logging: From Basic Setup to Enterprise Scale

Learn how to integrate Winston for efficient logging in Node.js. Explore features, configurations, and best practices to optimize your app's performance.

Jan 2nd, ‘25
Winston Logging in Node.js: The Essential Guide for Developers
See How Last9 Works

Unified observability for all your telemetry. Open standards. Simple pricing.

Talk to an Expert

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.

📖
For a deeper dive into logging and monitoring in Linux, check out our detailed guide on Syslog in the Linux Syslog Explained blog.

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 at info, warn, and error levels will be recorded.
  • format defines how logs are displayed. Here we’re using the simple 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.

💡
For a comprehensive understanding of when to use each level, see our detailed guide on Log Levels: Different Types and How to Use Them.

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 or error) so low-priority messages don’t consume resources.
  • Limit file size and count: Use maxsize and maxFiles 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:

  1. Circuit breakers — Temporarily disable non-critical logs during load spikes to protect system performance.
  2. 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.
  3. Dynamic log levels — Lower verbosity automatically when CPU/memory is high:
if (systemLoadHigh()) {
  logger.level = 'error'; // Only critical logs
}

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');
📖
For a deeper look at handling logs in distributed environments, our guide to cloud log management explains proven approaches to scaling, retention, and analysis that work in production.

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.

📖
If you’re comparing Winston with other logging options, our guide to Node.js logging libraries covers their strengths and trade-offs in detail.

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.

Probo Cuts Monitoring Costs by 90% with Last9
Probo Cuts Monitoring Costs by 90% with Last9

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
📝
For an in-depth look at structured logging in Python, don't miss our guide on Python Logging with Structlog.

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
📖
You can also check out our article on Systemctl Logs for insights into managing logs with systemd.

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.

📖
For more on logging in scheduled tasks, explore our article on Crontab Logs.

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.

Fix Winston logging issues in Node.js — right from your IDE, using AI and Last9 MCP.

Last 9 Mobile Illustration

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.

📖
For a practical walkthrough on capturing and alerting on error logs as they happen, check out our guide to monitoring error logs in real time.

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:

  1. Use Different Log Levels: Don’t log everything as info or error. Use a combination of debug, warn, and error to provide more meaningful insights, making it easier to filter logs by severity.
  2. Log Contextually: Include context in your logs, such as user IDs, request IDs, or other relevant details. This extra context can help with troubleshooting.
  3. 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.
  4. 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.
  5. 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.
  6. Monitor Logging Performance: Track memory usage and logging latency. Set alerts for when logging operations exceed acceptable thresholds (>10ms per operation).
  7. Implement Security Sanitization: Always scrub sensitive data (passwords, tokens, PII) before logging. Use custom formatters to automatically remove or mask sensitive fields.
  8. 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.
  9. 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.
📖
These practices align with our broader Logging Best Practices to Reduce Noise and Improve Insights.

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!

Authors
Anjali Udasi

Anjali Udasi

Helping to make the tech a little less intimidating. I

Contents

Do More with Less

Unlock unified observability and faster triaging for your team.