Robust Error Handling in Node.js: Patterns and Pitfalls

A payment processing API crashes at 2 AM because a database connection timed out. A user uploads malformed JSON, and your server returns a generic 500 error without logging details. These scenarios aren't just hypothetical—they're system vulnerabilities caused by incomplete error handling strategies. In Node.js applications, where asynchronous operations dominate, error management requires deliberate design rather than afterthought exception wraps.

Operational Errors vs Programmer Errors: Know Your Enemy

Not all errors are created equal. Operational errors represent runtime failures your code anticipates: failed API calls, invalid user input, or exhausted database connections. These require graceful recovery. Programmer errors—like undefined variables or incorrect function parameters—signal code flaws. They should crash the process immediately.

javascript
// Operational error: Handle programmatically
try {
  await connectToDatabase();
} catch (error) {
  logger.error('DB connection failed', error);
  retryWithBackoff();
}

// Programmer error: Crash and restart
if (typeof userId !== 'string') {
  process.exit(1); // Let process manager restart the app
}

Mixing these categories leads to unstable systems. Handle operational errors, but let programmer errors terminate the process to avoid undefined states.

Async/Await: Beyond Basic Try/Catch

While try/catch works with async functions, its misuse causes subtle failures. Consider a route handler fetching user data:

javascript
// Broken: The await is outside the try block
app.post('/user', async (req, res) => {
  try {
    const data = fetchData(); // Missing await!
    res.json(data);
  } catch (error) {
    res.status(500).send('Failed');
  }
});

This catches exceptions from fetchData() only if it's synchronous. For async operations, ensure await resides inside try:

javascript
app.post('/user', async (req, res) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (error) {
    res.status(500).send('Failed');
  }
});

Centralized Error Handling in Express

Scattering error responses across route handlers creates maintenance nightmares. Express middleware enables centralized error processing:

javascript
// Error-first middleware (note four arguments)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    error: {
      message: err.message,
      code: err.code,
      details: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }
  });
});

// Route handler propagates errors via next()
app.get('/product/:id', async (req, res, next) => {
  try {
    const product = await Product.find(req.params.id);
    if (!product) throw new AppError('Product not found', 404);
    res.json(product);
  } catch (error) {
    next(error); // Delegates to error middleware
  }
});

For async routes, avoid manual try/catch blocks by wrapping handlers with a utility like express-async-errors:

javascript
require('express-async-errors');
app.get('/order/:id', async (req, res) => {
  const order = await Order.find(req.params.id);
  if (!order) throw new AppError('Order missing', 404);
  res.json(order);
});

Unhandled Promise Rejections: The Silent Killers

Uncaught promise rejections terminate Node.js processes since v15. Monitor them globally:

javascript
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection at:', promise, 'Reason:', reason);
  // Optionally: Exit after logging
  process.exit(1);
});

In production, pair this with a process manager like PM2 or Kubernetes to auto-restart crashed instances.

Strategic Logging with Context

Basic console.error calls lack actionable details. Enhance logs with:

  • Timestamps
  • Error codes
  • Request IDs
  • Stack traces (in non-production environments)
javascript
// Winston logger configuration example
const logger = winston.createLogger({
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [new winston.transports.File({ filename: 'errors.log' })]
});

// Usage with request context
app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    path: req.path,
    method: req.method,
    user: req.user?.id,
    stack: err.stack
  });
  next(err);
});

Custom Error Classes for Richer Context

Generic Error instances force middleware to parse messages. Extend Error to classify issues:

javascript
class AppError extends Error {
  constructor(message, statusCode, code, details) {
    super(message);
    this.statusCode = statusCode;
    this.code = code; // Machine-readable identifier
    this.details = details; // Additional context
    Error.captureStackTrace(this, this.constructor);
  }
}

// Usage
throw new AppError(
  'Invalid subscription tier',
  400,
  'E_INVALID_SUBSCRIPTION',
  { userId: 123, plan: 'free' }
);

Middleware can then tailor responses based on error type and code.

Testing Error Paths: Don't Wait for Production

Use testing frameworks to validate error handling logic:

javascript
describe('User API', () => {
  it('returns 404 for missing users', async () => {
    const res = await request(app)
      .get('/user/non-existent-id')
      .expect(404);
    expect(res.body.error.code).to.equal('E_NOT_FOUND');
  });
});

Tools like Sinon help simulate operational errors (e.g., mocked database failures) to verify recovery paths.

Final Recommendations

  1. Fail fast: Crash on programmer errors; recover from operational ones
  2. Centralize: Use middleware or equivalent abstraction layers
  3. Decorate: Enrich errors with context for debuggability
  4. Monitor: Tools like Sentry or Datadog need structured error data
  5. Test: Treat error cases as first-class test scenarios

Error handling isn't about preventing failures—they're inevitable. The goal is to manage failures predictably, maintain observability, and minimize user impact. Invest in error infrastructure with the same rigor as feature development. Your midnight self will thank you.

text