most tutorials on error handling are either too basic or way too complicated. here's what actually works in production.
the problem
node.js crashes when you don't catch errors. simple as that.
app.get('/user/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
});
if db.findUser throws, your server dies. no response. no logs. just gone.
the obvious fix
wrap stuff in try-catch:
app.get('/user/:id', async (req, res) => {
try {
const user = await db.findUser(req.params.id);
res.json(user);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'something broke' });
}
});
works. but doing this everywhere gets old fast.
async wrapper
one function to rule them all:
const wrap = fn => (req, res, next) =>
fn(req, res, next).catch(next);
app.get('/user/:id', wrap(async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
}));
errors go to express error middleware. no more try-catch everywhere.
error middleware
one place to handle all errors:
app.use((error, req, res, next) => {
console.error(error);
const status = error.statusCode || 500;
const message = error.expose ? error.message : 'internal error';
res.status(status).json({ error: message });
});
that's it. clean and centralized.
custom errors if you want
sometimes useful:
class NotFound extends Error {
constructor(what = 'resource') {
super(`${what} not found`);
this.statusCode = 404;
this.expose = true;
}
}
// usage
if (!user) throw new NotFound('user');
not required but makes code cleaner.
global handlers
safety net for things that slip through:
process.on('unhandledRejection', (err) => {
console.error('unhandled rejection:', err);
});
process.on('uncaughtException', (err) => {
console.error('uncaught exception:', err);
process.exit(1);
});
if these fire often, you have a problem.
that's it
wrap async routes. have error middleware. add global handlers.
you don't need fancy error libraries or complex hierarchies. this handles 99% of cases.
stop overcomplicating it.