Let’s be honest for a second. We all hate writing error handling in Express.
You start with a beautiful, clean controller function. It’s supposed to do one thing: register a user. But then reality hits. You need to validate the body. You need to check if the email exists in MongoDB. You need to catch unexpected crashes.
Before you know it, your 5-line function has turned into a 50-line spaghetti monster wrapped in a massive try/catch block.
I used to copy-paste this boilerplate from project to project until I found a cleaner way. Recently, I started using ds-express-errors, and it actually made coding backend logic fun again.
Here is how I cleaned up my code.
The "Old Me" Approach 🍝
This is actual code from one of my old projects. It hurts just looking at it.
// controllers/authController.js
const User = require('../models/User');
exports.register = async (req, res, next) => {
try {
// 1. Manually checking for missing fields... boring.
if (!req.body.email || !req.body.password) {
return res.status(400).json({ status: 'error', message: 'Missing fields' });
}
const newUser = await User.create(req.body);
res.status(201).json({ success: true, data: newUser });
} catch (err) {
// 2. The "Error parsing hell"
// Is it a validation error? A duplicate key? Who knows?
if (err.name === 'ValidationError') {
const msg = Object.values(err.errors).map(val => val.message).join(', ');
return res.status(400).json({ success: false, message: msg });
}
if (err.code === 11000) {
return res.status(409).json({ success: false, message: 'Email already taken' });
}
// 3. Just giving up
console.error(err);
res.status(500).json({ success: false, message: 'Server Error' });
}
};
If you have 20 controllers, you have 20 copies of this catch block. If you change your error response format, you have to refactor everything.
The "New Me" Approach ✨
I switched to using ds-express-errors because it specifically handles the heavy lifting for libraries I already use (Mongoose, Prisma, Zod).
Here is the exact same controller rewritten.
// controllers/authController.js
const User = require('../models/User');
const { asyncHandler, Errors } = require('ds-express-errors'); //
// No try-catch. Just logic.
exports.register = asyncHandler(async (req, res) => {
const newUser = await User.create(req.body);
// Need to throw a custom error? Easy.
if (!newUser) {
throw Errors.BadRequest('Could not create user'); //
}
res.status(201).json({ success: true, data: newUser });
});
That’s it.
"Wait, what happens if MongoDB throws an error?"
That's the cool part. The library's middleware automatically detects that it's a Mongoose error.
- If it's a Duplicate Key (E11000) -> It sends a 400 Bad Request with "Duplicate field value entered".
- If it's a Validation Error -> It joins your validation messages and sends a 400.
- If it's a JWT Error -> It sends a 401 Unauthorized.
I didn't have to write a single if statement for that.
Setup is stupidly simple
I hate complex configs. Luckily, this is just a middleware drop-in.
In your app.js or index.js:
const express = require('express');
const { errorHandler } = require('ds-express-errors'); //
const app = express();
// ... define your routes here ...
// Just put this at the END of your middleware chain
app.use(errorHandler);
app.listen(3000, () => console.log('Server is flying 🚀'));
Bonus: Sleeping better at night (Graceful Shutdown) v1.5.0
Another thing I used to ignore was proper server shutdown. If my app crashed or Docker restarted the container, active requests would just get cut off. Users would lose data.
This lib has a built-in helper for this. It ensures the server closes connections properly before dying.
const { initGlobalHandlers, gracefulHttpClose } = require('ds-express-errors'); //
const server = app.listen(PORT);
initGlobalHandlers({
// Wraps the server close method safely
closeServer: gracefulHttpClose(server),
// Close your DB connections here
onShutdown: async () => {
console.log('Closing DB...');
await mongoose.disconnect();
}
});
Now, if I get a SIGTERM or an unhandled rejection, the app cleans up its mess before exiting.
Verdict
There are a million error handlers out there, but I stick with this one because it has zero dependencies and supports TypeScript out of the box. It just removes the noise from my code.
Link: ds-express-errors.dev
Top comments (0)