Logging in JavaScript Applications: Best Practices and Tips

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Logging in JavaScript Applications: Best Practices



Logging is not just about writing events to a file. In modern Node.js applications, it is a powerful tool for monitoring, debugging, and auditing. Without quality logging, finding the cause of a production server crash becomes a guessing game. In this article, we will break down the best practices for logging in JavaScript (Node.js) that will help you build reliable and maintainable systems.



Why Standard console.log is a Bad Practice



Many beginner developers use console.log for debugging. However, in production, this leads to problems:

  • Lack of levels: you cannot separate informational messages from critical errors.
  • No structure: console output is just a string that is difficult to parse.
  • Blocking I/O: synchronous writing can slow down the application.
  • No rotation: log files will grow indefinitely.

Instead, use specialized libraries such as winston or pino. They provide flexible configuration of transports (output to file, console, external services) and asynchronous writing.



Choosing a Logging Library: winston vs pino



There are two clear leaders in the Node.js ecosystem. Let's look at their features:



Winston

The most popular library. It supports many transports (file, HTTP, MongoDB, Elasticsearch) and custom formats. It is well-suited for monolithic applications.

const winston = require('winston');

const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console({ format: winston.format.simple() }) ]});

logger.info('Server started on port 3000');logger.error('Database connection error', { db: 'mongo', code: 500 });


Pino

An ultra-fast library that is 5-10 times faster than winston. It is ideal for high-load systems and microservices. It outputs JSON by default.

const pino = require('pino');

const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino/file', options: { destination: './app.log' } }});

logger.info({ user: 'admin' }, 'User logged into the system');logger.error(new Error('Timeout'), 'Request error');


Tip: for most projects, choose winston because of its flexibility. If performance is critical, use pino.



Logging Levels: How Not to Drown in Messages



Proper use of levels is the foundation of good logging. In Node.js, the following hierarchy is accepted (from least to most critical):

  • trace / silly — extremely detailed debugging information (usually disabled in production).
  • debug — debugging messages for developers.
  • info — confirmation of normal operation (requests, service startups).
  • warn — an abnormal situation, but the application continues to work (e.g., exceeding request limits).
  • error — an error requiring attention (request failure, external service outage).
  • fatal — a critical error after which the application cannot continue working.

Configure the logging level through environment variables:

const LOG_LEVEL = process.env.LOG_LEVEL || 'info';const logger = winston.createLogger({ level: LOG_LEVEL });

Set debug in development, and info in production.

Blogs

Book Recommendations