Logging in JavaScript Applications: Best Practices for Beginners
Logging is the foundation of debugging and monitoring any application. Without quality logs, you'll be looking for a "needle in a haystack" when something goes wrong. In this article, we'll cover the best practices for logging in JavaScript (Node.js), suitable for both small projects and production environments.
1. Why You Shouldn't Rely on console.log?
Many beginner developers use console.log for everything. It's convenient during development, but in a production environment, this approach leads to problems:
- No logging levels — you can't filter errors from informational messages.
- Difficult to manage output — you can't disable logs for specific modules.
- Poor performance — serializing objects in
console.logcan be costly. - Lack of structure — logs don't have a unified format (JSON, timestamp, level).
Instead, use specialized libraries. Here's a simple but correct example using Winston:
// logger.jsconst 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' }), ],});
if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.simple(), }));}
module.exports = logger;2. Logging Levels: When to Use What
Standard logging levels (in increasing severity): debug, info, warn, error. It's important to understand when to apply each level:
- debug — detailed information for developers (variable values, system state). Disabled in production.
- info — normal application events (request processed, user registered).
- warn — potentially dangerous situations (low memory, deprecated API).
- error — errors that require intervention (exceptions, database failures).
Example of correct usage with the Pino library (the fastest for Node.js):
const pino = require('pino');
const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { colorize: true } }});
// Usagelogger.info({ userId: 123 }, 'User logged in');logger.error(new Error('Database connection error'), 'Critical error');logger.debug('Variable x = 42'); // won't output if level = 'info'3. Structured Logging: JSON and Context
Instead of text strings, use structured logs (usually JSON). This allows you to:
- Easily parse logs with tools (ELK, Graylog).
- Add context (request ID, user, version).
- Filter and aggregate data.
Example of a structured log with context:
const logger = require('./logger');
app.use((req, res, next) => { req.requestId = uuidv4(); next();});
app.get('/api/users', async (req, res) => { logger.info({ requestId: req.requestId, method: req.method, url: req.originalUrl, ip: req.ip }, 'Incoming request');
try { const users = await getUsers(); logger.info({ requestId: req.requestId, count: users.length }, 'Users retrieved'); res.json(users); } catch (err) { logger.error({