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.