DEV Community

Cover image for How One Middleware Fixed 90% of Our Node.js Bugs
Arunangshu Das
Arunangshu Das

Posted on

How One Middleware Fixed 90% of Our Node.js Bugs

If you're building backend services in Node.js, you’ve probably faced bugs that feel like they appear out of nowhere—silent failures, unexpected crashes, or just inconsistent behavior that doesn't show up in local testing. We certainly did. Our team had been working on a relatively complex Node.js backend powering APIs for a client-facing web dashboard. But no matter how many unit tests we wrote or code reviews we did, strange issues kept creeping into production.

Then, we made a small but powerful change: we introduced one middleware function. It didn’t refactor our logic or change any major architecture. It simply caught what was already there—errors hiding in plain sight. To our surprise, that one middleware fixed over 90% of our runtime bugs.

Let’s break it all down—what the middleware was, why it worked, and how you can implement it in your own projects to dramatically improve stability and maintainability.

The Context: A Growing Node.js Codebase

When we first started building our Node.js backend, we followed the standard practices:

  • Express.js for routing
  • MongoDB with Mongoose for data persistence
  • JSON-based REST APIs
  • A handful of routes for CRUD operations

It started simple. But over time, as we added features—authentication, complex data filtering, pagination, file uploads, rate limiting, and so on—the bugs began multiplying. Here are just a few examples of the issues we faced:

  • Silent crashes: API would randomly fail and just return a 500 error.
  • Uncaught promise rejections: Async functions inside route handlers failed silently.
  • Lack of consistent error formatting: Logs were a mess, hard to trace.
  • Multiple exit points in functions: Hard to debug stack traces.
  • Nested try/catch blocks: Quickly turned messy and inconsistent.

The scary part? These weren’t bugs you could catch with a linter. And even worse, many were intermittent.

The Turning Point: Introducing Centralized Error-Handling Middleware

We stepped back and asked ourselves: “Where are all these errors actually happening?”

The answer? Almost always inside asynchronous route handlers—those little async (req, res) => {} functions we casually throw into Express routes. They might look clean, but if anything throws an error and it’s not properly try/catch'd, Express just doesn’t know what to do with it. The request hangs, or worse, crashes the app.

Then, we discovered the solution that changed everything.

The Middleware That Saved Us

const asyncHandler = fn => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};
Enter fullscreen mode Exit fullscreen mode

That’s it. Just five lines.

We wrapped every single route handler in asyncHandler. Suddenly, every unhandled rejection and every async bug that used to slip through the cracks was now being caught and passed down to our centralized error handler.

And from there, logging and debugging became predictable, consistent, and manageable.

How We Used It in Practice

Before:

app.get('/api/users', async (req, res) => {
  const users = await User.find(); // what if this throws?
  res.json(users);
});
Enter fullscreen mode Exit fullscreen mode

If User.find() throws, Express won’t catch it. The app might crash or hang.

After:

app.get('/api/users', asyncHandler(async (req, res) => {
  const users = await User.find(); 
  res.json(users);
}));
Enter fullscreen mode Exit fullscreen mode

Now, if anything fails in the async block, the asyncHandler catches it and forwards it to Express's next() function, which invokes the error-handling middleware.

Setting Up the Error Handling Middleware

Once you use asyncHandler, you need a good error handler. Here’s how we set that up:

app.use((err, req, res, next) => {
  console.error(err.stack);
  
  res.status(err.statusCode || 500).json({
    success: false,
    message: err.message || 'Server Error',
  });
});
Enter fullscreen mode Exit fullscreen mode

This final piece ensures that every thrown error—no matter where it originates—is:

  • Logged properly (with stack trace)
  • Responded with consistent JSON
  • Never crashes the server

Now our logs actually meant something, and we weren’t getting “undefined” errors in the console anymore.

The Results: 90% Fewer Bugs, Faster Debugging

Within just a couple of weeks of rolling out this pattern across all routes, here’s what we saw:

Issue Type                  Before Middleware After Middleware
Silent promise rejections   Frequent          Almost never    
Route handlers crashing app Occasional        Rare            
Logs with incomplete errors Often             Fixed           
Dev time spent debugging    High              Reduced by 70%+ 
Bugs reported by QA         Constant          Minimal         

We ran an internal audit of the bugs resolved over the past few months and estimated that 90% of them were directly related to unhandled async behavior or poor error formatting. This middleware solved both.

Why This Middleware Works So Well

Let’s dissect why such a tiny middleware is so impactful:

1. It Centralizes Error Handling

You no longer need to remember to wrap every await in a try/catch. Your brain gets a break from repetitive boilerplate.

2. It Prevents Crashes

Unhandled promise rejections can crash Node.js in strict environments. This middleware makes your server resilient.

3. It Enhances Maintainability

Instead of debugging 20 separate handler files, you can look at a single error-handling file and make improvements in one place.

4. It Standardizes Error Responses

Clients consuming your API will always get a proper JSON error, instead of HTML dumps or hanging requests.

5. It’s Framework-Agnostic

This pattern works with any Express-like middleware-based Node.js framework—Koa, Fastify, NestJS (with some tweaks), and more.

Bonus: Create Your Own Custom Error Classes

After the success of asyncHandler, we took it a step further. We defined our own AppError class for structured error throwing:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
 
    Error.captureStackTrace(this, this.constructor);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now instead of throw new Error("User not found"), we could do:

throw new AppError("User not found", 404);
Enter fullscreen mode Exit fullscreen mode

And our centralized middleware could use that to send more accurate status codes, improving both client and developer experience.

Pro Tips for Using This Middleware Effectively

  1. Wrap ALL route handlers: One missing wrap can cause elusive bugs.
  2. Use with custom error classes: For clear and concise status management.
  3. Add a logging service: Use winston, pino, or send logs to Loggly/Datadog.
  4. Use it with validation middleware: Like express-validator, and throw structured errors on invalid input.
  5. Avoid mixing try/catch unless necessary: Let the async handler do the heavy lifting.

What If You're Using Other Frameworks?

While Express is the most common use case, this pattern or variation of it can be applied elsewhere:

  • NestJS: Use Exception Filters or wrap controllers with interceptors.
  • Koa: Use try/catch in middleware or use koa-router that supports async error propagation.
  • Hapi.js: Errors thrown in handlers are captured by default; still, centralized logging helps.

Real-World Example: Fixing a Critical Production Bug

Let’s wrap this up with a real story.

We had a payment route that interacted with Stripe. Occasionally, users would get charged but the response would fail, leading to panic.

Original code:

app.post('/api/charge', async (req, res) => {
  const charge = await stripe.charges.create({
    amount: 2000,
    currency: 'usd',
    source: req.body.tokenId,
  });
 
  await saveToDB(charge); // This sometimes failed
 
  res.status(200).json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

If saveToDB(charge) failed, no response was sent. Users thought payments were broken, even when they went through.

After fix:

app.post('/api/charge', asyncHandler(async (req, res) => {
  const charge = await stripe.charges.create({
    amount: 2000,
    currency: 'usd',
    source: req.body.tokenId,
  });
 
  await saveToDB(charge);
 
  res.status(200).json({ success: true });
}));
Enter fullscreen mode Exit fullscreen mode

Now, if anything fails, it’s caught, logged, and the client gets a structured error with success: false. No more ghost bugs.

Final Thoughts: Small Changes, Big Wins

Backend engineering is full of small, quiet bugs that eat up your time and energy. But sometimes, the best solutions aren’t massive rewrites or frameworks—they’re simple, consistent practices like this middleware.

TL;DR

  • Node.js async errors often go unhandled in Express routes.
  • One middleware (asyncHandler) catches all async errors and forwards them to Express.
  • It reduces crashes, makes debugging easier, and improves consistency.
  • Combine it with custom error classes and centralized error logging for best results.
  • This small change fixed over 90% of our app's runtime bugs.

You may also like:

  1. Top 10 Large Companies Using Node.js for Backend

  2. Why 85% of Developers Use Express.js Wrongly

  3. Top 10 Node.js Middleware for Efficient Coding

  4. 5 Key Differences: Worker Threads vs Child Processes in Node.js

  5. 5 Effective Caching Strategies for Node.js Applications

  6. 5 Mongoose Performance Mistakes That Slow Your App

  7. Building Your Own Mini Load Balancer in Node.js

  8. 7 Tips for Serverless Node.js API Deployment

  9. How to Host a Mongoose-Powered App on Fly.io

  10. The Real Reason Node.js Is So Fast

  11. 10 Must-Know Node.js Patterns for Application Growth

  12. How to Deploy a Dockerized Node.js App on Google Cloud Run

  13. Can Node.js Handle Millions of Users?

  14. How to Deploy a Node.js App on Vercel

  15. 6 Common Misconceptions About Node.js Event Loop

  16. 7 Common Garbage Collection Issues in Node.js

  17. How Do I Fix Performance Bottlenecks in Node.js?

  18. What Are the Advantages of Serverless Node.js Solutions?

  19. High-Traffic Node.js: Strategies for Success

Read more blogs from Here

Share your experiences in the comments, and let's discuss how to tackle them!

Follow me on LinkedIn

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.