
In today’s fast-paced digital landscape, reliable and secure email communication is essential for businesses and developers alike. Whether you’re sending password recovery links, notifications, or logs of critical application events, it’s crucial to ensure that your email infrastructure is functioning correctly. This is where having robust logging in place becomes vital. In the context of Node.js applications, combining Nodemailer with Winston provides a powerful toolkit for managing emails and logging relevant events with precision.
Why Email Logging Matters
Email is a critical method of communication between your system and your users. When emails fail to send or are sent incorrectly, it can lead to disastrous consequences such as missed deadlines, security issues, or disgruntled users. Implementing email logging helps you:
- Monitor your email traffic for successful sends and failures
- Diagnose delivery issues quickly by checking timestamped logs
- Improve your debugging process during development and production
- Maintain a reliable audit trail of all email communications
In this article, you will learn how to set up email logging in your Node.js application by integrating Nodemailer for sending emails and Winston for managing logs.
Setting the Stage: Installing Dependencies
Before diving in, ensure that your Node.js environment is ready. Then, install the necessary packages using npm:
npm install nodemailer winston
Nodemailer allows you to send emails from Node.js applications, while Winston is a versatile logging library with support for multiple transports like files, consoles, or external services.
Step 1: Configure Winston Logger
Let’s start by configuring Winston to log both to the console and a file. This ensures high visibility during development and long-term traceability in production.
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'email.log' })
]
});
module.exports = logger;
In this configuration, logs are written in structured JSON with a timestamp, providing a readable and searchable format. The above setup logs everything at the info level and higher, which is ideal for capturing email events.
Step 2: Set Up Nodemailer
Now that Winston is set, it’s time to configure Nodemailer to send emails. Use either an SMTP configuration or a test account from Ethereal for development:
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: {
user: 'your_email@example.com',
pass: 'your_email_password'
}
});
You can test this setup with a simple function:
async function sendTestEmail() {
let info = await transporter.sendMail({
from: '"Your App" ',
to: 'recipient@example.com',
subject: 'Test Email',
text: 'This is a test email',
html: '<b>This is a test email</b>'
});
console.log('Message sent: %s', info.messageId);
}
Make sure your SMTP credentials are correct and that your sender address is verified if using a service like SendGrid, Mailgun, or Amazon SES.

Step 3: Integrate Email Events with Winston Logging
This is where Winston shines. We’ll capture the success or failure of email delivery using Nodemailer’s response and log these details accordingly. Modify your sendTestEmail
function like so:
const logger = require('./logger');
async function sendLoggedEmail() {
try {
let info = await transporter.sendMail({
from: '"Your App" <your_email@example.com>',
to: 'recipient@example.com',
subject: 'Logged Email',
text: 'This email was sent with logging enabled.',
});
logger.info({
message: 'Email sent successfully',
messageId: info.messageId,
to: info.envelope.to,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error({
message: 'Failed to send email',
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
}
}
This dual logging of success and failure ensures you never lose sight of what your application is sending or where it is failing. This becomes vitally important when scaling systems or complying with data audit requirements.
Advanced Logging Techniques
Winston can be extended to support daily log rotation, log levels by environment (dev, test, prod), and even external services like Loggly or Amazon CloudWatch. Here’s how you can take logging one step further:
- Add Daily Rotate File transport:
npm install winston-daily-rotate-file
- Add metadata such as userId or requestId to logs
- Structure logs depending on environment: set shorter logs for local dev, and detailed logs for production
Here is a sample enhancement using environment-based log levels:
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'warn' : 'info',
...
});
This ensures your production server doesn’t get flooded with harmless info logs, while development environments benefit from verbose outputs.

Error Handling Best Practices
Always wrap your email sending code inside try-catch
blocks to capture potential runtime issues. Additionally, log contextual data like which user triggered the email, what action initiated it, and whether a retry might be needed. The more context your logs have, the easier it will be to resolve issues when they arise.
Here’s how that might look:
async function sendEmailWithContext(userId, toEmail) {
try {
let info = await transporter.sendMail({
to: toEmail,
subject: 'Account Notification',
text: 'Hello, your account settings have changed.',
});
logger.info({
message: 'Account change email sent',
userId: userId,
email: toEmail,
messageId: info.messageId
});
} catch (err) {
logger.error({
message: 'Error sending account change email',
userId: userId,
email: toEmail,
error: err.message
});
}
}
Testing and Validation
Test your logging setup across different scenarios:
- Email sent successfully
- SMTP authentication failure
- Recipient email address invalid
- Internet or DNS issues
Each of these use cases should generate a clearly formatted log that helps your team understand and resolve the issue in real time.
Final Thoughts
Combining Nodemailer and Winston gives you robust control over email operations and logging in your Node.js application. By capturing thorough logs of every email event, you enhance your system’s reliability and make troubleshooting a much more efficient process.
Remember, a professional-grade application always includes detailed logging—not just for security and compliance, but to enhance maintainability and developer productivity. Integrate logging early into your development lifecycle to avoid surprises down the road.
Next Steps
- Consider integrating retry mechanisms for temporary failures
- Monitor email bounce rates and include bounce logs
- Use Winston’s capabilities to push logs to external dashboards for better visibility
By following the strategies outlined in this guide, you’ll be well on your way to implementing a resilient, audit-ready email infrastructure powered by Node.js, Nodemailer, and Winston.