π This was originally posted on blog.rashedin.dev
When building a backend with Node.js and Express, we're likely using async/await
to handle things like database queries or API calls.
But thereβs a catch β if we donβt handle errors properly, our server can crash or behave unpredictably. π¬
In this post, you'll learn a clean, scalable way to handle async errors in Express:
- Why
try-catch
in every route is painful - How to fix it with a reusable
asyncHandler()
- How to simplify this using external libraries
- How to use my own package: express-error-toolkit
- How to define custom error classes
- And how to set up a global error handler
π¨ The Problem With Try-Catch Everywhere
Hereβs how we usually handle errors:
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
} catch (error) {
next(error);
}
});
Repeating this in every route is:
- Redundant
- Ugly
- Easy to forget
Letβs fix that.
β
Option 1: Write a Custom asyncHandler
// utils/asyncHandler.js
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = asyncHandler;
Use it like this:
const asyncHandler = require('../utils/asyncHandler');
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new Error('User not found');
res.json(user);
}));
Clean. Reusable. No try-catch
.
π¦ Option 2: Use a Library (Highly Recommended)
πΉ express-error-toolkit β View on npm
I built this package to make error handling in Express apps much easier. It includes:
- An
asyncHandler()
function - Predefined error classes (
NotFoundError
,BadRequestError
, etc.) - A global error-handling middleware
- Clean stack traces in development
Install
npm install express-error-toolkit
Use
const { asyncHandler } = require('express-error-toolkit');
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new Error('User not found');
res.json(user);
}));
π§± Define Custom Error Classes
If you donβt use a package, you can define your own:
// utils/ApiError.js
class ApiError extends Error {
constructor(statusCode, message) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ApiError;
Usage:
const ApiError = require('../utils/ApiError');
if (!user) throw new ApiError(404, 'User not found');
Or use express-error-toolkitβs built-in errors
const { NotFoundError } = require('express-error-toolkit');
if (!user) throw new NotFoundError('User not found');
π Global Error-Handling Middleware
Add this at the end of your middleware chain:
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(status).json({
success: false,
message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
});
Or use express-error-toolkitβs built-in handler:
const { globalErrorHandler } = require('express-error-toolkit');
app.use(globalErrorHandler);
π§ͺ Full Example
const express = require('express');
const mongoose = require('mongoose');
const {
NotFoundError,
asyncHandler,
globalErrorHandler,
} = require('express-error-toolkit');
const app = express();
app.use(express.json());
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User not found');
res.json(user);
}));
app.use(globalErrorHandler);
app.listen(3000, () => console.log('Server running on port 3000'));
π§ Final Thoughts
β
Avoid try-catch
in every route using asyncHandler
π¦ Use express-error-toolkit for a full-featured, clean setup
π§± Throw meaningful errors with custom classes
π Catch and format all errors in one global middleware
Follow this approach and your Express backend will be clean, scalable, and production-ready. π
Top comments (8)
i've alr handle just simple like these
Thanks for sharing your approach! π
Your setup looks clean and nicely modular with the errorLogger and errorResponder pattern β I like that you're logging and handling separately.
If you're ever looking to simplify the async error handling part as well (without wrapping every route in try-catch), feel free to check out express-error-toolkit β it's a small package I built that does just that, plus it includes prebuilt error classes and a global handler. Could be a nice addition to your setup. π
Would love to hear more about how you're using req.my_func for diagnostics β that looks interesting!
very cool, the asyncHandler and error toolkit both make life so much easier for bigger projects
you think adding more specialized error types actually helps with debugging in production or just adds noise
Totally agree β once a project starts growing, these small abstractions make a big difference! π
As for specialized error types β Iβve found they definitely help in production, as long as theyβre used intentionally. For example:
NotFoundError, ValidationError, or AuthError help quickly categorize issues in logs or monitoring tools
They make it easier to return consistent error responses from the global handler
And in complex apps (e.g., with microservices or role-based logic), they help avoid relying on brittle if/else chains
That said, I try to keep the number of custom types lean β just enough to cover core use cases without becoming noise.
Curious to hear your thoughts β do you prefer fewer general errors or more specific ones?
So much cleaner with asyncHandler! Have you ever hit any weird edge cases with this pattern in production?
Absolutely agree β asyncHandler really helps keep things clean! π
In my experience so far, itβs been quite reliable even in production environments. No major edge cases yet, especially when paired with a solid global error handler. That said, one thing to watch out for is throwing non-Error values (like plain strings or objects) β itβs always safer to throw an Error or a custom error class to keep the stack trace consistent.
Also, if you're using third-party middlewares that throw errors outside the async context, those might still need special attention.
Let me know if you've run into any tricky situations!
I've tried express-async-error package before, I will try yours one for my future project.
Always tried an utils function to avoid try-catch, never heard of the package name you mentioned. It may increase my DX.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.