Say Goodbye to Try-Catch: Smarter Async Error Handling in Express

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-catchin 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. 🚀






