Currently viewing the human version
Switch to AI version

Node.js Error Handling: Lessons From 1000 Production Fires

Node.js Logo

Error handling workflow in Node.js applications

The First Rule of Node.js Production: Everything Will Break

Let me tell you about the worst 6 hours of my life. It was 2:30 AM on a Friday (because it's always a Friday), our payment processing was down, and the only error I had was:

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'id' of undefined
    at processPayment (/app/payment.js:47:23)

No request ID. No user context. No clue which payment was failing. Just 500,000 angry customers and a CEO who was surprisingly coherent for someone I'd just woken up.

That night taught me that Node.js error handling isn't about elegant code—it's about not getting fired.

What I Learned the Hard Way: Two Types of Fuckups

After debugging hundreds of outages, every Node.js error falls into two buckets:

Expected Fuckups (Operational Errors)
These will happen. Plan for them:

  • Database craps out (AWS RDS has feelings apparently)
  • Users send garbage JSON ("age": "twenty-five" broke our signup for 3 hours)
  • Third-party APIs randomly return 503 (looking at you, Stripe)
  • Your Docker container runs out of memory (again)

Unexpected Fuckups (Programmer Errors)
These are on you:

The "Oh Shit" Decision Tree

When something breaks at 3 AM:

  1. Operational error? → Log it, handle it, keep running, fix the root cause Monday
  2. Programmer error? → Log it, restart the process, fix it NOW before more users hit the same bug

This took me 2 years and countless sleepless nights to figure out. The restart part is crucial—corrupted state will spread like cancer.

Error Classes That Don't Suck

JavaScript's built-in Error is basically useless for debugging production issues. Here's what actually works after trying 50 different approaches:

The One Error Class You Actually Need

// errors/AppError.js - Yes, one file. Keep it simple.
class AppError extends Error {
  constructor(message, statusCode = 500, context = {}) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.context = context;
    this.timestamp = new Date().toISOString();
    this.isOperational = statusCode < 500; // 4xx = user error, 5xx = our error

    Error.captureStackTrace(this, AppError);
  }
}

module.exports = AppError;

How to Use It (Copy-Paste Ready)

// When user sends bad data
throw new AppError('User ID must be a number', 400, { userId: req.params.id });

// When database is down
throw new AppError('Database connection failed', 500, {
  operation: 'getUserById',
  userId: 123
});

// When external API fails
throw new AppError('Payment processor unavailable', 502, {
  service: 'stripe',
  paymentId: payment.id
});

I tried building fancy error hierarchies - ValidationError, DatabaseError, all that bullshit. Spent two weeks building this beautiful inheritance tree. Know what happened? My team couldn't remember which error to throw when. We ended up with DatabaseValidationNetworkError nonsense. One error class with good context is infinitely better than 10 error classes that confuse your team.

Error Handling Pattern:

The key insight is separating operational errors (network failures, bad user input) from programmer errors (null pointer exceptions, syntax errors). Handle the first, crash fast on the second.

Async/Await: The Only Pattern That Matters

Forget everything you know about callbacks. Here's the one pattern that has saved my ass countless times:

Controller Pattern (Copy This)

// controllers/userController.js
const AppError = require('../errors/AppError');

exports.getUser = async (req, res, next) => {
  try {
    const { id } = req.params;

    // This will blow up if id is \"undefined\" - ask me how I know
    if (!id || isNaN(parseInt(id))) {
      throw new AppError('User ID must be a number', 400, { providedId: id });
    }

    // Always set timeouts. PostgreSQL will hang forever otherwise.
    const user = await User.findById(parseInt(id), { timeout: 5000 });

    if (!user) {
      throw new AppError('User not found', 404, { userId: id });
    }

    res.json({ user });

  } catch (error) {
    next(error); // Let middleware handle it
  }
};

The timeout part is critical. I once had a query hang for 8 hours because someone forgot a WHERE clause. 8 fucking hours. Watching PostgreSQL slowly eat all our connection pools while I tried to figure out why checkout was broken. 5 seconds is generous for most operations.

Retry Logic That Won't Piss Off Your APIs

Circuit Breaker Pattern:

When external services fail, implement a circuit breaker: try → fail fast → wait → try again. This prevents cascading failures.

// Don't retry everything. Stripe will ban you.
async function processPayment(paymentData) {
  const maxRetries = 3;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await stripe.charges.create(paymentData);
    } catch (error) {
      // 4xx errors = user problem, don't retry
      if (error.statusCode < 500 && attempt === maxRetries) {
        throw new AppError(`Payment failed: ${error.message}`, 400, {
          stripeError: error.code,
          paymentData: { amount: paymentData.amount } // Don't log card details!
        });
      }

      // 5xx errors = their problem, maybe retry
      if (error.statusCode >= 500 && attempt < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Linear backoff is fine
        continue;
      }

      throw new AppError('Payment processor unavailable', 502, {
        attempt,
        stripeError: error.code
      });
    }
  }
}

Exponential backoff is overkill for most use cases. Linear backoff works fine and won't make you wait 16 seconds on the third retry.

Error Middleware That Won't Drive You Insane

Here's the error handler I use everywhere. It's 30 lines and handles 99% of cases:

// middleware/errorHandler.js
const AppError = require('../errors/AppError');

function errorHandler(error, req, res, next) {
  // Log everything. You'll thank me later.
  console.error('Error:', {
    message: error.message,
    stack: error.stack,
    url: req.url,
    method: req.method,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    requestId: req.id,
    context: error.context || {}
  });

  // Figure out status code
  let statusCode = 500;
  let message = 'Something broke. We\'re looking into it.';

  if (error instanceof AppError) {
    statusCode = error.statusCode;
    message = error.message;
  } else if (error.name === 'ValidationError') {
    statusCode = 400;
    message = error.message;
  } else if (error.code === 'ECONNREFUSED') {
    statusCode = 503;
    message = 'Database connection failed';
  }

  // Don't leak internal errors to users in production
  if (statusCode >= 500 && process.env.NODE_ENV === 'production') {
    message = 'Internal server error';
  }

  res.status(statusCode).json({
    error: message,
    requestId: req.id
  });
}

module.exports = errorHandler;

That's it. No fancy abstractions. No metadata objects. Just log the error and return something useful to the client.

The Nuclear Option: Process Handlers

These catch the shit that crashes your app. Set them up once and forget about them:

// app.js - Put this at the very top
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason);
  // In Node 15+, this will crash your app anyway. Log it first.
  process.exit(1);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1); // Something is very wrong. Just die.
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received. Shutting down...');
  server.close(() => {
    process.exit(0);
  });
});

Do NOT try to recover from these. If you get an uncaught exception, your process is corrupted. Log it and restart. That's what PM2 or Docker is for.

I've seen teams spend days trying to "gracefully handle" uncaught exceptions. It's a waste of time. Just crash fast and restart clean.

Request IDs: The One Thing That Will Save Your Sanity

Add this middleware and thank me later when you're debugging at 3 AM:

// middleware/requestId.js
const crypto = require('crypto');

function addRequestId(req, res, next) {
  req.id = req.headers['x-request-id'] || crypto.randomBytes(8).toString('hex');
  res.setHeader('X-Request-ID', req.id);
  next();
}

module.exports = addRequestId;

Now every error log will have a request ID. When a user reports "I got an error," you can find their exact request in the logs instead of playing guessing games.

The Bottom Line

Most error handling advice is theoretical bullshit. This stuff works because I've debugged 1000+ production incidents. Keep it simple, log everything with context, and restart when shit hits the fan. Your future self will thank you.

Additional Resources That Don't Suck

Questions That Actually Come From Real Stack Overflow Posts

Q

Why does my Node.js app keep crashing with UnhandledPromiseRejectionWarning?

A

Time to fix: 10 minutes if you know what you're doing, 3 hours of Stack Overflow hell if you don't

This happens when you forget `.catch()` on a Promise. Node 15+ will murder your app for this - no warning, straight to exit code 1.

Most common cause (90% of cases):

// This will crash your app
app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id); // Missing try/catch
  res.json(user);
});

The 30-second fix:

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    res.json(user);
  } catch (error) {
    next(error); // Let error middleware handle it
  }
});

Pro tip: If you see this in production, add the nuclear option to your app.js:

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled Promise:', reason);
  process.exit(1); // Just crash and restart
});
Q

My database keeps timing out and killing my API. How do I fix this?

A

Time to fix: 20 minutes for basic solution, 2 hours if you need circuit breakers

Your database is probably fine. Your connection handling is probably fucked.

The problem: You're not setting timeouts, so when PostgreSQL hangs, your API hangs forever.

The fix:

// Add timeouts to EVERYTHING
const pool = new Pool({
  connectionTimeoutMillis: 5000,  // Die after 5 seconds
  idleTimeoutMillis: 30000,       // Close idle connections
  max: 10                         // Don't let pg eat all your memory
});

async function getUser(id) {
  const client = await pool.connect();
  try {
    // Set query timeout too
    const result = await client.query('SELECT * FROM users WHERE id = $1', [id]);
    return result.rows[0];
  } catch (error) {
    throw new AppError(`Database query failed: ${error.message}`, 500);
  } finally {
    client.release(); // ALWAYS release the connection
  }
}

Pro tip: If you keep hitting timeouts, your queries are probably shit. Use `EXPLAIN ANALYZE` to see what's actually happening.

Q

Should I restart Node.js when errors happen?

A

Time to understand: 5 minutes, time to implement properly: 1 hour

Short answer: Only when your app is fucked beyond repair.

Don't restart for user errors:

  • Invalid JSON from client (400)
  • Database temporarily down (503)
  • Third-party API being flaky (502)
  • User uploaded 50GB file (413)

DO restart for programmer errors:

  • Memory leaks eating all your RAM
  • Uncaught exceptions (your code is broken)
  • Event loop blocked for >10 seconds
  • Heap out of memory

The simple rule:

// In your error handler
if (statusCode >= 500 && error.name === 'Error') {
  // Programmer error - restart
  console.error('Programmer error detected, restarting');
  process.exit(1);
}

Let PM2 or Docker handle the restart. Don't try to be clever about it.

Q

My error logs are useless garbage. How do I make them actually help me debug?

A

Time to fix: 30 minutes to set up, saves 2+ hours per bug

Your logs suck because they don't tell you who, what, when, where.

Useless log (what you're probably doing):

console.error('Database error');

Useful log (copy this pattern):

console.error('Database query failed', {
  requestId: req.id,           // Track this specific request
  userId: req.user?.id,        // Which user broke it
  query: 'getUserById',        // What operation failed
  error: error.message,        // What went wrong
  stack: error.stack,          // Where it went wrong
  url: req.url,               // What endpoint
  userAgent: req.get('User-Agent') // What browser/client
});

The 5 things every error log needs:

  1. Request ID - so you can find all logs for one user's request
  2. User ID - so you know which users are affected
  3. What operation - was it a database query? API call? file upload?
  4. Error message - the actual error from the system
  5. Stack trace - so you know exactly which line of code broke
Q

Help! I'm logging passwords and credit cards. How do I stop?

A

Time to fix: 15 minutes, time to recover from data breach: 6 months and your job

The nuclear approach (just delete the sensitive shit):

function sanitizeForLogging(obj) {
  const sanitized = { ...obj };

  // Just delete these fields entirely
  delete sanitized.password;
  delete sanitized.credit_card;
  delete sanitized.ssn;
  delete sanitized.authorization;
  delete sanitized.cookie;
  delete sanitized.token;

  return sanitized;
}

// Use it everywhere
console.error('Request failed', {
  body: sanitizeForLogging(req.body),
  headers: sanitizeForLogging(req.headers)
});

Never log this shit:

  • Passwords (duh)
  • Credit card numbers
  • API keys and tokens
  • SSNs or government IDs
  • Anything you wouldn't want in a data breach report
Q

Should I use callbacks or async/await for error handling?

A

Time to decide: 2 seconds. Always use async/await.

If you're writing new code in 2024 and using callbacks, you're doing it wrong.

Callbacks are from 2012. Use this pattern:

// Modern (what you should do)
async function getUser(id) {
  try {
    const user = await User.findById(id);
    return user;
  } catch (error) {
    throw new AppError(`Failed to get user: ${error.message}`, 500);
  }
}

Don't do this (callback hell):

// 2012 style - don't do this
function getUser(id, callback) {
  User.findById(id, (error, user) => {
    if (error) return callback(error);
    callback(null, user);
  });
}

Exception: You're stuck with a legacy library that only supports callbacks. In that case, wrap it in a Promise and move on with your life.

Q

Express middleware errors are breaking my app. How do I fix them?

A

Time to fix: 10 minutes

Always call next(error) or your app will hang:

app.use(async (req, res, next) => {
  try {
    // Do your middleware stuff
    req.user = await getUser(req.headers.authorization);
    next(); // Continue to next middleware
  } catch (error) {
    next(error); // CRITICAL: Pass error to error handler
  }
});

// Error handler (put this LAST)
app.use((error, req, res, next) => {
  console.error('Middleware error:', error);
  res.status(error.statusCode || 500).json({
    error: error.message
  });
});

Common mistake: Forgetting to call `next(error)`. Your request will hang forever.

Q

How do I know when my app is having problems?

A

Time to set up: 30 minutes for basic monitoring

Start simple - just count 500 errors:

let errorCount = 0;

// In your error handler
app.use((error, req, res, next) => {
  if (res.statusCode >= 500) {
    errorCount++;

    // Simple alert - more than 10 errors in the last minute
    if (errorCount > 10) {
      console.error('HIGH ERROR RATE ALERT:', errorCount);
      // Send to Slack, email, etc.
    }
  }

  // Reset counter every minute
  setTimeout(() => errorCount = 0, 60000);
});

Alerts that actually matter:

  • More than 10 server errors (5xx) in 1 minute
  • Any error on your payment endpoints
  • Database connection failures
  • Memory usage above 85%

Don't alert on everything. You'll get alert fatigue and start ignoring them all.

Q

Does error handling slow down my app?

A

Short answer: No. Crashing slows down your app.

Good error handling prevents crashes, which cost way more than microseconds of overhead. The performance impact is negligible:

  • Creating an error object: ~0.01ms
  • Logging with context: ~0.1ms
  • Stack trace capture: ~0.05ms

Compare that to:

  • App crash and restart: 5-30 seconds of downtime
  • Debugging without good logs: 2+ hours of engineer time
  • Lost customers from crashes: Priceless (and not in a good way)

Just don't log massive objects and you'll be fine.

Node.js Monitoring: Why Most Teams Get It Wrong

Key Monitoring Metrics:

Focus on the metrics that actually matter: error rates, response times, and memory usage. Everything else is noise until you get these basics right.

Your Monitoring Probably Sucks (And That's Normal)

Here's how monitoring actually works in the real world:

  1. Your app goes down at 2 AM
  2. Customers email you 4 hours later asking "Is your site broken?"
  3. You check your monitoring dashboard - everything looks green
  4. You realize you've been monitoring CPU usage while your database connections are leaking
  5. You spend the weekend setting up "comprehensive monitoring"
  6. Your phone now buzzes every 10 minutes with false alerts about disk space
  7. You turn off the alerts
  8. Goto step 1

I've done this dance at 5 different companies. Here's what actually works.

The Only 3 Things You Actually Need to Monitor

Forget the "observability maturity model" bullshit. Monitor these 3 things:

  1. Are users getting errors? (HTTP 5xx rate)
  2. Is the app slow? (Response time P95)
  3. Is it about to crash? (Memory usage, event loop lag)

That's it. Everything else is vanity metrics that look good in demos but won't wake you up when your payment processing dies.

Start here - 10 minutes to set up:

// Simple monitoring that actually works
let last500Count = 0;
let totalRequests = 0;

app.use((req, res, next) => {
  totalRequests++;

  res.on('finish', () => {
    if (res.statusCode >= 500) {
      last500Count++;
    }

    // Alert if error rate > 5%
    if (totalRequests > 20 && (last500Count / totalRequests) > 0.05) {
      console.error('HIGH ERROR RATE:', last500Count, 'out of', totalRequests);
      // Send alert to Slack/email
    }
  });

  next();
});

// Reset every 5 minutes
setInterval(() => {
  last500Count = 0;
  totalRequests = 0;
}, 5 * 60 * 1000);

Memory Monitoring (The Thing That Actually Kills Your App)

Check memory every 30 seconds or you'll get OOMKilled:

// This will save your ass
setInterval(() => {
  const usage = process.memoryUsage();
  const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
  const heapTotalMB = Math.round(usage.heapTotal / 1024 / 1024);

  console.log(`Memory: ${heapUsedMB}MB used, ${heapTotalMB}MB total`);

  // Alert if using more than 400MB (adjust for your container limits)
  if (heapUsedMB > 400) {
    console.error('HIGH MEMORY USAGE ALERT:', heapUsedMB, 'MB');
    // This is when you restart the process
  }
}, 30000);

Real talk: Memory leaks are the #1 cause of Node.js crashes in production. Found this out the hard way when our user service kept OOMKilling every 3 days back in December 2023. Turned out someone was caching user sessions in a Map() without expiration. 50k users = bye bye memory. Monitor this or your app will die a slow death.

Memory Leak Detection:

Node.js memory usage should stay relatively stable. If it keeps climbing, you have a leak. Set alerts when heap usage exceeds 80% of your container limit.

Response Time Monitoring (Keep It Simple)

Track slow requests that piss off users:

app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;

    // Log slow requests (anything over 2 seconds is bad)
    if (duration > 2000) {
      console.warn(`SLOW REQUEST: ${req.method} ${req.url} took ${duration}ms`);
    }

    // Alert if more than 10% of requests are slow
    // (You'll need to track this over time)
  });

  next();
});

Rule of thumb:

  • Under 200ms = good
  • 200-1000ms = acceptable
  • Over 1000ms = users start to notice
  • Over 3000ms = users leave

Don't overthink percentiles. Just track requests over 2 seconds and fix those first.

Event Loop Monitoring (For When Your App Freezes)

Check if your app is blocked:

// Super simple event loop check
setInterval(() => {
  const start = process.hrtime();

  setImmediate(() => {
    const delta = process.hrtime(start);
    const lagMs = (delta[0] * 1000) + (delta[1] * 1e-6);

    if (lagMs > 100) {
      console.warn(`Event loop lag: ${lagMs.toFixed(2)}ms`);
      // Something is blocking your app
    }
  });
}, 5000);

If you see consistent lag over 100ms, you're probably:

The fix: Move CPU work to worker threads or child processes.

Event Loop Health:

Event loop lag above 100ms means your app is blocked. Users will notice sluggish responses. Profile your code to find the bottleneck.

Health Checks (So Your Load Balancer Knows You're Alive)

Simple health check endpoint:

app.get('/health', async (req, res) => {
  try {
    // Check database connection
    await db.query('SELECT 1');

    // Check memory isn't too high
    const usage = process.memoryUsage();
    const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);

    if (heapUsedMB > 400) {
      throw new Error(`High memory usage: ${heapUsedMB}MB`);
    }

    res.status(200).json({
      status: 'healthy',
      memory: `${heapUsedMB}MB`,
      uptime: Math.floor(process.uptime())
    });

  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

Use this for:

The Bottom Line on Monitoring

Start with these 4 alerts:

  1. HTTP 5xx error rate > 5%
  2. Memory usage > 400MB (or 80% of your container limit)
  3. Response time > 2 seconds for more than 10% of requests
  4. Health check fails

Don't alert on:

  • CPU usage (unless it's constantly 100%)
  • Disk space (unless you're actually writing lots of files)
  • Network I/O (unless you know what normal looks like)
  • 4xx errors (those are user errors)

Monitoring that actually works:

  • Logs everything to stdout (let your log aggregator handle the rest)
  • Simple health check endpoint
  • Basic memory and response time tracking
  • Alerts that wake you up for real problems only

The expensive lesson I learned: More monitoring doesn't mean better monitoring. I've seen teams with 50+ alerts where every single one was ignored because of alert fatigue. Start simple, add complexity only when you need it.

More Resources That Actually Help

Node.js Monitoring Tools: What Actually Works (And What Doesn't)

Tool

What It's Good For

Why You'd Choose It

Why You Wouldn't

Real Cost

My Honest Take

Datadog

Teams with money who want everything in one place

Auto-instruments most libraries, decent Node.js support, nice dashboards

Expensive as hell, pricing calculator gives me nightmares

$15-50/host/month, but can easily hit $500+/month (we hit $1,200/month at 15 containers)

Good if your company can afford it. Otherwise you'll spend more time optimizing costs than actual monitoring

New Relic

Companies already using it for other stuff

Dead simple setup, reasonable Node.js support

Gets expensive fast, some overhead on performance

Starts at $99/month, scales with data ingestion

Used it at 3 companies. Works fine but nothing special. Price creep is real

Prometheus + Grafana

Teams with time to maintain their own monitoring

Free, total control, great for custom metrics

You become the monitoring team, tons of setup

Free software, expensive engineer time

Love the flexibility, hate the maintenance. Great if you have dedicated DevOps

console.log + PM2

Small teams or side projects

It's already in your code, PM2 gives basic process monitoring

No alerting, hard to search logs, not scalable

$0

Honestly this handles 80% of what you need for small apps

Winston + ELK

When you need centralized logging but want to own it

Structured logging, powerful search in Elasticsearch

ELK maintenance is a full-time job

Hosting costs vary, setup time is huge

Great for logs, terrible for metrics. Plan for a week of Elasticsearch hell

Sentry

Error tracking (not full monitoring)

Amazing for catching errors you didn't know about

Only errors, not performance metrics

$26/month for small teams

Every Node.js app should use this. Just for errors though

Node.js Logging That Doesn't Suck

Logging Levels Strategy:

Use ERROR for things that break your app, WARN for things that might break it, INFO for things users do, DEBUG for everything else.

Why Your Logs Are Probably Garbage

Let me guess: your production logs look like this:

Server started
User logged in
Error: Something broke
Database error
User logged out

Good luck debugging anything with that. When shit hits the fan at 3 AM, you need logs that actually tell you what happened, who was affected, and why.

Here's how to fix it without turning into the over-engineering police.

Winston: Just Use This Config

Winston is fine. Here's a config that works without 50 lines of bullshit:

// logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console()
  ]
});

module.exports = logger;

That's it. 15 lines. It logs JSON to console, which is what you want 90% of the time.

If you're running in development:

// Add this if you want pretty colors in development
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }));
}

HTTP Request Logging (Keep It Simple)

Use Morgan to log HTTP requests. Don't overthink it:

// app.js
const morgan = require('morgan');

// Log all requests in JSON format
app.use(morgan('combined', {
  skip: (req) => req.url === '/health' // Skip health checks
}));

If you want structured JSON logs:

// Slightly fancier but still simple
app.use(morgan(':method :url :status :response-time ms', {
  stream: {
    write: (message) => {
      logger.info('HTTP Request', { log: message.trim() });
    }
  }
}));

Pro tip: Don't log every single request header. You don't need that much data, and it'll cost you money in log storage.

HTTP Request Logging:

Log the essentials: method, URL, status code, response time, and request ID. Skip headers unless you're debugging specific issues.

Request IDs (The Thing That Saves You at 3 AM)

Add a request ID to every request. When users report problems, you can find their exact request:

// middleware/requestId.js
const crypto = require('crypto');

function addRequestId(req, res, next) {
  req.id = req.headers['x-request-id'] || crypto.randomBytes(8).toString('hex');
  res.setHeader('X-Request-ID', req.id);
  next();
}

// Use it everywhere
app.use(addRequestId);

// Log with request ID
app.use((req, res, next) => {
  logger.info('Request started', {
    requestId: req.id,
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent')
  });
  next();
});

When users report errors: "I got an error at 2:30 PM" becomes "Find request ID abc123def in the logs." Boom, you know exactly what happened.

Request Correlation:

Every request gets a unique ID that follows it through your entire system. When users report problems, you can trace their exact journey through your logs.

Don't Log Sensitive Shit

I once spent a weekend rotating API keys because someone logged authorization headers in production. Don't be that person.

Simple rule: don't log anything you wouldn't want in a data breach report.

// Simple sanitizer
function sanitizeForLogging(obj) {
  if (!obj) return obj;

  const sanitized = { ...obj };

  // Just delete the obvious stuff
  delete sanitized.password;
  delete sanitized.credit_card;
  delete sanitized.ssn;
  delete sanitized.authorization;
  delete sanitized.cookie;
  delete sanitized.token;

  return sanitized;
}

// Use it everywhere
logger.error('Request failed', {
  requestId: req.id,
  body: sanitizeForLogging(req.body),
  error: error.message
});

What not to log:

  • Passwords
  • Credit card numbers
  • API keys and tokens
  • SSNs or government IDs
  • Anything that would get you fired if it leaked

The Bottom Line on Logging

What you actually need:

  1. Winston with JSON format
  2. Request IDs on every request
  3. Don't log sensitive data
  4. Log to stdout, let Docker/Kubernetes handle the rest

What you don't need (until you actually need it):

  • Complex log aggregation
  • Fancy log analysis tools
  • Performance optimizations
  • Real-time log processing

The progression that actually works:

  1. Start: Winston + console logs
  2. Growing: Add Sentry for errors
  3. Scaling: Add log aggregation (ELK, Splunk, etc.)
  4. Enterprise: Add all the fancy stuff your compliance team wants

Most important rule: Consistent structure is better than fancy features. Use the same log format everywhere, include request IDs, and you'll be able to debug 95% of issues.

Don't over-engineer logging. It's supposed to help you debug problems, not become a problem itself.

Additional Logging Resources

Resources That Actually Help (And Which Ones to Skip)

Related Tools & Recommendations

tool
Similar content

Node.js Production Deployment - How to Not Get Paged at 3AM

Optimize Node.js production deployment to prevent outages. Learn common pitfalls, PM2 clustering, troubleshooting FAQs, and effective monitoring for robust Node

Node.js
/tool/node.js/production-deployment
98%
tool
Similar content

Node.js Production Debugging - Fix The Shit That Actually Breaks

When your Node.js app crashes at 3 AM, here's how to find the real problem fast

Node.js
/tool/node.js/production-debugging
96%
tool
Similar content

Node.js Memory Leaks and Debugging - Stop Your App From Crashing at 3am

Learn to identify and debug Node.js memory leaks, prevent 'heap out of memory' errors, and keep your applications stable. Explore common patterns, tools, and re

Node.js
/tool/node.js/debugging-memory-leaks
95%
tool
Similar content

Node.js Microservices - Why Your Team Probably Fucked It Up

Learn why Node.js microservices projects often fail and discover practical strategies to build robust, scalable distributed systems. Avoid common pitfalls and e

Node.js
/tool/node.js/microservices-architecture
95%
tool
Similar content

Docker for Node.js - The Setup That Doesn't Suck

Master Node.js Docker containerization with this comprehensive guide. Learn why Docker matters, optimize your builds, and implement advanced patterns for robust

Node.js
/tool/node.js/docker-containerization
94%
tool
Similar content

Node.js Deployment: Stop Breaking Production at 3AM

Master Node.js deployment strategies, from traditional servers to modern serverless and containers. Learn to optimize CI/CD pipelines and prevent production iss

Node.js
/tool/node.js/deployment-strategies
94%
tool
Similar content

Node.js Production Troubleshooting - Debug the Shit That Breaks at 3AM

When your Node.js app crashes in production and nobody knows why. The complete survival guide for debugging real-world disasters.

Node.js
/tool/node.js/production-troubleshooting
70%
tool
Similar content

Node.js WebSocket Scaling Past 20k Connections

WebSocket tutorials show you 10 users. Production has 20k concurrent connections and shit breaks.

Node.js
/tool/node.js/realtime-websocket-scaling
70%
integration
Similar content

Claude API + Express.js - Production Integration Guide

Stop fucking around with tutorials that don't work in production

Claude API
/integration/claude-api-nodejs-express/complete-implementation-guide
64%
integration
Similar content

MongoDB + Express + Mongoose Production Deployment

Deploy Without Breaking Everything (Again)

MongoDB
/integration/mongodb-express-mongoose/production-deployment-guide
64%
tool
Popular choice

jQuery - The Library That Won't Die

Explore jQuery's enduring legacy, its impact on web development, and the key changes in jQuery 4.0. Understand its relevance for new projects in 2025.

jQuery
/tool/jquery/overview
60%
tool
Similar content

Deploy Hono Apps Without Breaking Production

Master Hono production deployment. Learn best practices for monitoring, database connections, and environment variables to ensure your Hono apps run stably and

Hono
/tool/hono/production-deployment
59%
howto
Similar content

Migrating from Node.js to Bun Without Losing Your Sanity

Because npm install takes forever and your CI pipeline is slower than dial-up

Bun
/howto/migrate-nodejs-to-bun/complete-migration-guide
58%
tool
Popular choice

Hoppscotch - Open Source API Development Ecosystem

Fast API testing that won't crash every 20 minutes or eat half your RAM sending a GET request.

Hoppscotch
/tool/hoppscotch/overview
57%
integration
Similar content

Claude API Code Execution Integration - Advanced Tools Guide

Build production-ready applications with Claude's code execution and file processing tools

Claude API
/integration/claude-api-nodejs-express/advanced-tools-integration
57%
troubleshoot
Similar content

Your Traces Are Fucked and Here's How to Fix Them

When distributed tracing breaks in production and you're debugging blind

OpenTelemetry
/troubleshoot/microservices-distributed-tracing-failures/common-tracing-failures
56%
tool
Popular choice

Stop Jira from Sucking: Performance Troubleshooting That Works

Frustrated with slow Jira Software? Learn step-by-step performance troubleshooting techniques to identify and fix common issues, optimize your instance, and boo

Jira Software
/tool/jira-software/performance-troubleshooting
55%
integration
Similar content

Deploying MERN Apps Without Losing Your Mind

The deployment guide I wish existed 5 years ago

MongoDB
/integration/mern-stack-production-deployment/production-cicd-pipeline
53%
tool
Similar content

Node.js Ecosystem Integration 2025 - When JavaScript Took Over Everything

Node.js went from "JavaScript on the server? That's stupid" to running half the internet. Here's what actually works in production versus what looks good in dem

Node.js
/tool/node.js/ecosystem-integration-2025
53%
tool
Popular choice

Northflank - Deploy Stuff Without Kubernetes Nightmares

Discover Northflank, the deployment platform designed to simplify app hosting and development. Learn how it streamlines deployments, avoids Kubernetes complexit

Northflank
/tool/northflank/overview
52%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization