Most Node.js error handling tutorials show you try/catch.
That's not enough for production.
I learned this the hard way. A silent catch block swallowed a database failure. My API returned 200. The user's data never saved. Nobody knew for 48 hours.
After that incident, I built a pattern I now wire into every Express + TypeScript REST API I ship. It has four parts. Every part earns its place.
Here's exactly what it looks like โ and why each piece matters.
๐ง๐ต๐ฒ ๐ฝ๐ฟ๐ผ๐ฏ๐น๐ฒ๐บ ๐๐ถ๐๐ต ๐ฑ๐ฒ๐ณ๐ฎ๐๐น๐ ๐๐
๐ฝ๐ฟ๐ฒ๐๐ ๐ฒ๐ฟ๐ฟ๐ผ๐ฟ ๐ต๐ฎ๐ป๐ฑ๐น๐ถ๐ป๐ด
Out of the box, Express does this when something throws:
๐ข๐ฑ๐ฑ.๐จ๐ฆ๐ต('/๐ถ๐ด๐ฆ๐ณ๐ด/:๐ช๐ฅ', ๐ข๐ด๐บ๐ฏ๐ค (๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด) => {
๐ค๐ฐ๐ฏ๐ด๐ต ๐ถ๐ด๐ฆ๐ณ = ๐ข๐ธ๐ข๐ช๐ต ๐ฅ๐ฃ.๐ง๐ช๐ฏ๐ฅ๐๐บ๐๐ฅ(๐ณ๐ฆ๐ฒ.๐ฑ๐ข๐ณ๐ข๐ฎ๐ด.๐ช๐ฅ); // ๐ต๐ฉ๐ณ๐ฐ๐ธ๐ด? ๐ค๐ณ๐ข๐ด๐ฉ๐ฆ๐ด ๐ต๐ฉ๐ฆ ๐ฑ๐ณ๐ฐ๐ค๐ฆ๐ด๐ด
๐ณ๐ฆ๐ด.๐ซ๐ด๐ฐ๐ฏ(๐ถ๐ด๐ฆ๐ณ);
});
If db.findById throws, Express doesn't catch it in async route handlers by default. Your server either crashes or hangs. The client waits forever.
Even if you add try/catch:
๐ข๐ฑ๐ฑ.๐จ๐ฆ๐ต('/๐ถ๐ด๐ฆ๐ณ๐ด/:๐ช๐ฅ', ๐ข๐ด๐บ๐ฏ๐ค (๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด) => {
๐ต๐ณ๐บ {
๐ค๐ฐ๐ฏ๐ด๐ต ๐ถ๐ด๐ฆ๐ณ = ๐ข๐ธ๐ข๐ช๐ต ๐ฅ๐ฃ.๐ง๐ช๐ฏ๐ฅ๐๐บ๐๐ฅ(๐ณ๐ฆ๐ฒ.๐ฑ๐ข๐ณ๐ข๐ฎ๐ด.๐ช๐ฅ);
๐ณ๐ฆ๐ด.๐ซ๐ด๐ฐ๐ฏ(๐ถ๐ด๐ฆ๐ณ);
} ๐ค๐ข๐ต๐ค๐ฉ (๐ฆ๐ณ๐ณ) {
๐ณ๐ฆ๐ด.๐ด๐ต๐ข๐ต๐ถ๐ด(500).๐ซ๐ด๐ฐ๐ฏ({ ๐ฆ๐ณ๐ณ๐ฐ๐ณ: ๐ฆ๐ณ๐ณ.๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ }); // ๐ญ๐ฆ๐ข๐ฌ๐ด ๐ช๐ฏ๐ต๐ฆ๐ณ๐ฏ๐ข๐ญ๐ด
}
});
Now you're leaking internal error messages to clients, losing stack traces in logs, and duplicating error logic across every route.
Here's what I do instead.
๐ฃ๐ฎ๐ฟ๐ ๐ญ: ๐ ๐๐๐ฝ๐ฒ๐ฑ ๐๐ฝ๐ฝ๐๐ฟ๐ฟ๐ผ๐ฟ ๐ฐ๐น๐ฎ๐๐
Every error in the system extends a single base class. No raw new Error() anywhere in the codebase.
// ๐ด๐ณ๐ค/๐ฆ๐ณ๐ณ๐ฐ๐ณ๐ด/๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ.๐ต๐ด
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ค๐ญ๐ข๐ด๐ด ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ ๐ฆ๐น๐ต๐ฆ๐ฏ๐ฅ๐ด ๐๐ณ๐ณ๐ฐ๐ณ {
๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ(
๐ฑ๐ถ๐ฃ๐ญ๐ช๐ค ๐ณ๐ฆ๐ข๐ฅ๐ฐ๐ฏ๐ญ๐บ ๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: ๐ด๐ต๐ณ๐ช๐ฏ๐จ,
๐ฑ๐ถ๐ฃ๐ญ๐ช๐ค ๐ณ๐ฆ๐ข๐ฅ๐ฐ๐ฏ๐ญ๐บ ๐ด๐ต๐ข๐ต๐ถ๐ด๐๐ฐ๐ฅ๐ฆ: ๐ฏ๐ถ๐ฎ๐ฃ๐ฆ๐ณ,
๐ฑ๐ถ๐ฃ๐ญ๐ช๐ค ๐ณ๐ฆ๐ข๐ฅ๐ฐ๐ฏ๐ญ๐บ ๐ค๐ฐ๐ฅ๐ฆ: ๐ด๐ต๐ณ๐ช๐ฏ๐จ,
๐ฑ๐ถ๐ฃ๐ญ๐ช๐ค ๐ณ๐ฆ๐ข๐ฅ๐ฐ๐ฏ๐ญ๐บ ๐ช๐ด๐๐ฑ๐ฆ๐ณ๐ข๐ต๐ช๐ฐ๐ฏ๐ข๐ญ: ๐ฃ๐ฐ๐ฐ๐ญ๐ฆ๐ข๐ฏ = ๐ต๐ณ๐ถ๐ฆ
) {
๐ด๐ถ๐ฑ๐ฆ๐ณ(๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ);
๐ต๐ฉ๐ช๐ด.๐ฏ๐ข๐ฎ๐ฆ = ๐ต๐ฉ๐ช๐ด.๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ.๐ฏ๐ข๐ฎ๐ฆ;
๐๐ณ๐ณ๐ฐ๐ณ.๐ค๐ข๐ฑ๐ต๐ถ๐ณ๐ฆ๐๐ต๐ข๐ค๐ฌ๐๐ณ๐ข๐ค๐ฆ(๐ต๐ฉ๐ช๐ด, ๐ต๐ฉ๐ช๐ด.๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ);
}
}
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ค๐ญ๐ข๐ด๐ด ๐๐ฐ๐ต๐๐ฐ๐ถ๐ฏ๐ฅ๐๐ณ๐ณ๐ฐ๐ณ ๐ฆ๐น๐ต๐ฆ๐ฏ๐ฅ๐ด ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ {
๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ(๐ณ๐ฆ๐ด๐ฐ๐ถ๐ณ๐ค๐ฆ: ๐ด๐ต๐ณ๐ช๐ฏ๐จ) {
๐ด๐ถ๐ฑ๐ฆ๐ณ(${๐ณ๐ฆ๐ด๐ฐ๐ถ๐ณ๐ค๐ฆ} ๐ฏ๐ฐ๐ต ๐ง๐ฐ๐ถ๐ฏ๐ฅ, 404, '๐๐๐_๐๐๐๐๐');
}
}
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ค๐ญ๐ข๐ด๐ด ๐๐ข๐ญ๐ช๐ฅ๐ข๐ต๐ช๐ฐ๐ฏ๐๐ณ๐ณ๐ฐ๐ณ ๐ฆ๐น๐ต๐ฆ๐ฏ๐ฅ๐ด ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ {
๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ(๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: ๐ด๐ต๐ณ๐ช๐ฏ๐จ) {
๐ด๐ถ๐ฑ๐ฆ๐ณ(๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ, 422, '๐๐๐๐๐๐๐๐๐๐_๐๐๐๐๐');
}
}
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ค๐ญ๐ข๐ด๐ด ๐๐ฏ๐ข๐ถ๐ต๐ฉ๐ฐ๐ณ๐ช๐ป๐ฆ๐ฅ๐๐ณ๐ณ๐ฐ๐ณ ๐ฆ๐น๐ต๐ฆ๐ฏ๐ฅ๐ด ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ {
๐ค๐ฐ๐ฏ๐ด๐ต๐ณ๐ถ๐ค๐ต๐ฐ๐ณ() {
๐ด๐ถ๐ฑ๐ฆ๐ณ('๐๐ฏ๐ข๐ถ๐ต๐ฉ๐ฐ๐ณ๐ช๐ป๐ฆ๐ฅ', 401, '๐๐๐๐๐๐๐๐๐๐ก๐๐');
}
}
isOperational is the key flag. true means this is an expected failure โ a user not found, a bad payload. false means something unexpected broke โ a DB connection dropped, a third-party SDK crashed. Your error handler treats these differently.
๐ฃ๐ฎ๐ฟ๐ ๐ฎ: ๐ ๐ฐ๐ฒ๐ป๐๐ฟ๐ฎ๐น ๐ฒ๐ฟ๐ฟ๐ผ๐ฟ ๐ต๐ฎ๐ป๐ฑ๐น๐ฒ๐ฟ ๐บ๐ถ๐ฑ๐ฑ๐น๐ฒ๐๐ฎ๐ฟ๐ฒ
// ๐ด๐ณ๐ค/๐ฎ๐ช๐ฅ๐ฅ๐ญ๐ฆ๐ธ๐ข๐ณ๐ฆ/๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ.๐ต๐ด
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต, ๐๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ด๐ฆ, ๐๐ฆ๐น๐ต๐๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ } ๐ง๐ณ๐ฐ๐ฎ '๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '../๐ฆ๐ณ๐ณ๐ฐ๐ณ๐ด/๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '../๐ญ๐ช๐ฃ/๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ';
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ง๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ ๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ(
๐ฆ๐ณ๐ณ: ๐๐ณ๐ณ๐ฐ๐ณ,
๐ณ๐ฆ๐ฒ: ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต,
๐ณ๐ฆ๐ด: ๐๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ด๐ฆ,
๐ฏ๐ฆ๐น๐ต: ๐๐ฆ๐น๐ต๐๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ
): ๐ท๐ฐ๐ช๐ฅ {
๐ค๐ฐ๐ฏ๐ด๐ต ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ = ๐ณ๐ฆ๐ฒ.๐ฉ๐ฆ๐ข๐ฅ๐ฆ๐ณ๐ด['๐น-๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต-๐ช๐ฅ'] ๐ข๐ด ๐ด๐ต๐ณ๐ช๐ฏ๐จ;
๐ช๐ง (๐ฆ๐ณ๐ณ ๐ช๐ฏ๐ด๐ต๐ข๐ฏ๐ค๐ฆ๐ฐ๐ง ๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ && ๐ฆ๐ณ๐ณ.๐ช๐ด๐๐ฑ๐ฆ๐ณ๐ข๐ต๐ช๐ฐ๐ฏ๐ข๐ญ) {
// ๐๐น๐ฑ๐ฆ๐ค๐ต๐ฆ๐ฅ ๐ฆ๐ณ๐ณ๐ฐ๐ณ โ ๐ญ๐ฐ๐จ ๐ญ๐ช๐จ๐ฉ๐ต๐ญ๐บ, ๐ณ๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ฅ ๐ค๐ญ๐ฆ๐ข๐ฏ๐ญ๐บ
๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ.๐ธ๐ข๐ณ๐ฏ({
๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ,
๐ค๐ฐ๐ฅ๐ฆ: ๐ฆ๐ณ๐ณ.๐ค๐ฐ๐ฅ๐ฆ,
๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: ๐ฆ๐ณ๐ณ.๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ,
๐ด๐ต๐ข๐ต๐ถ๐ด๐๐ฐ๐ฅ๐ฆ: ๐ฆ๐ณ๐ณ.๐ด๐ต๐ข๐ต๐ถ๐ด๐๐ฐ๐ฅ๐ฆ,
});
๐ณ๐ฆ๐ด.๐ด๐ต๐ข๐ต๐ถ๐ด(๐ฆ๐ณ๐ณ.๐ด๐ต๐ข๐ต๐ถ๐ด๐๐ฐ๐ฅ๐ฆ).๐ซ๐ด๐ฐ๐ฏ({
๐ด๐ต๐ข๐ต๐ถ๐ด: '๐ฆ๐ณ๐ณ๐ฐ๐ณ',
๐ค๐ฐ๐ฅ๐ฆ: ๐ฆ๐ณ๐ณ.๐ค๐ฐ๐ฅ๐ฆ,
๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: ๐ฆ๐ณ๐ณ.๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ,
});
๐ณ๐ฆ๐ต๐ถ๐ณ๐ฏ;
}
// ๐๐ฏ๐ฆ๐น๐ฑ๐ฆ๐ค๐ต๐ฆ๐ฅ ๐ฆ๐ณ๐ณ๐ฐ๐ณ โ ๐ญ๐ฐ๐จ ๐ฆ๐ท๐ฆ๐ณ๐บ๐ต๐ฉ๐ช๐ฏ๐จ, ๐ณ๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ฅ ๐จ๐ฆ๐ฏ๐ฆ๐ณ๐ช๐ค๐ข๐ญ๐ญ๐บ
๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ.๐ฆ๐ณ๐ณ๐ฐ๐ณ({
๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ,
๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: ๐ฆ๐ณ๐ณ.๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ,
๐ด๐ต๐ข๐ค๐ฌ: ๐ฆ๐ณ๐ณ.๐ด๐ต๐ข๐ค๐ฌ,
๐ฏ๐ข๐ฎ๐ฆ: ๐ฆ๐ณ๐ณ.๐ฏ๐ข๐ฎ๐ฆ,
});
๐ณ๐ฆ๐ด.๐ด๐ต๐ข๐ต๐ถ๐ด(500).๐ซ๐ด๐ฐ๐ฏ({
๐ด๐ต๐ข๐ต๐ถ๐ด: '๐ฆ๐ณ๐ณ๐ฐ๐ณ',
๐ค๐ฐ๐ฅ๐ฆ: '๐๐๐๐๐๐๐๐_๐๐๐๐๐',
๐ฎ๐ฆ๐ด๐ด๐ข๐จ๐ฆ: '๐๐ฐ๐ฎ๐ฆ๐ต๐ฉ๐ช๐ฏ๐จ ๐ธ๐ฆ๐ฏ๐ต ๐ธ๐ณ๐ฐ๐ฏ๐จ. ๐๐ญ๐ฆ๐ข๐ด๐ฆ ๐ต๐ณ๐บ ๐ข๐จ๐ข๐ช๐ฏ.',
});
}
Clients never see stack traces, internal messages, or database query strings. Logs capture everything you need to debug. The requestId ties the log line back to the exact request that failed.
๐ฃ๐ฎ๐ฟ๐ ๐ฏ: ๐๐ป ๐ฎ๐๐๐ป๐ฐ ๐๐ฟ๐ฎ๐ฝ๐ฝ๐ฒ๐ฟ ๐๐ผ ๐ฐ๐ฎ๐๐ฐ๐ต ๐ฟ๐ผ๐๐๐ฒ ๐ฒ๐ฟ๐ฟ๐ผ๐ฟ๐
Express doesn't catch errors thrown inside async route handlers unless you explicitly call next(err). Writing try/catch in every route is noisy and forgettable.
This wrapper handles it once:
// ๐ด๐ณ๐ค/๐ถ๐ต๐ช๐ญ๐ด/๐ข๐ด๐บ๐ฏ๐ค๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ.๐ต๐ด
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต, ๐๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ด๐ฆ, ๐๐ฆ๐น๐ต๐๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ, ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด';
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ง๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ ๐ข๐ด๐บ๐ฏ๐ค๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ(๐ง๐ฏ: ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ): ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ {
๐ณ๐ฆ๐ต๐ถ๐ณ๐ฏ (๐ณ๐ฆ๐ฒ: ๐๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต, ๐ณ๐ฆ๐ด: ๐๐ฆ๐ด๐ฑ๐ฐ๐ฏ๐ด๐ฆ, ๐ฏ๐ฆ๐น๐ต: ๐๐ฆ๐น๐ต๐๐ถ๐ฏ๐ค๐ต๐ช๐ฐ๐ฏ) => {
๐๐ณ๐ฐ๐ฎ๐ช๐ด๐ฆ.๐ณ๐ฆ๐ด๐ฐ๐ญ๐ท๐ฆ(๐ง๐ฏ(๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด, ๐ฏ๐ฆ๐น๐ต)).๐ค๐ข๐ต๐ค๐ฉ(๐ฏ๐ฆ๐น๐ต);
};
}
Now routes look like this:
// ๐ด๐ณ๐ค/๐ณ๐ฐ๐ถ๐ต๐ฆ๐ด/๐ถ๐ด๐ฆ๐ณ๐ด.๐ต๐ด
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐๐ฐ๐ถ๐ต๐ฆ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐ข๐ด๐บ๐ฏ๐ค๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '../๐ถ๐ต๐ช๐ญ๐ด/๐ข๐ด๐บ๐ฏ๐ค๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐๐ฐ๐ต๐๐ฐ๐ถ๐ฏ๐ฅ๐๐ณ๐ณ๐ฐ๐ณ } ๐ง๐ณ๐ฐ๐ฎ '../๐ฆ๐ณ๐ณ๐ฐ๐ณ๐ด/๐๐ฑ๐ฑ๐๐ณ๐ณ๐ฐ๐ณ';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐ถ๐ด๐ฆ๐ณ๐๐ฆ๐ณ๐ท๐ช๐ค๐ฆ } ๐ง๐ณ๐ฐ๐ฎ '../๐ด๐ฆ๐ณ๐ท๐ช๐ค๐ฆ๐ด/๐ถ๐ด๐ฆ๐ณ๐๐ฆ๐ณ๐ท๐ช๐ค๐ฆ';
๐ค๐ฐ๐ฏ๐ด๐ต ๐ณ๐ฐ๐ถ๐ต๐ฆ๐ณ = ๐๐ฐ๐ถ๐ต๐ฆ๐ณ();
๐ณ๐ฐ๐ถ๐ต๐ฆ๐ณ.๐จ๐ฆ๐ต('/:๐ช๐ฅ', ๐ข๐ด๐บ๐ฏ๐ค๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ(๐ข๐ด๐บ๐ฏ๐ค (๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด) => {
๐ค๐ฐ๐ฏ๐ด๐ต ๐ถ๐ด๐ฆ๐ณ = ๐ข๐ธ๐ข๐ช๐ต ๐ถ๐ด๐ฆ๐ณ๐๐ฆ๐ณ๐ท๐ช๐ค๐ฆ.๐ง๐ช๐ฏ๐ฅ๐๐บ๐๐ฅ(๐ณ๐ฆ๐ฒ.๐ฑ๐ข๐ณ๐ข๐ฎ๐ด.๐ช๐ฅ);
๐ช๐ง (!๐ถ๐ด๐ฆ๐ณ) {
๐ต๐ฉ๐ณ๐ฐ๐ธ ๐ฏ๐ฆ๐ธ ๐๐ฐ๐ต๐๐ฐ๐ถ๐ฏ๐ฅ๐๐ณ๐ณ๐ฐ๐ณ('๐๐ด๐ฆ๐ณ'); // ๐ง๐ญ๐ฐ๐ธ๐ด ๐ด๐ต๐ณ๐ข๐ช๐จ๐ฉ๐ต ๐ต๐ฐ ๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ
}
๐ณ๐ฆ๐ด.๐ซ๐ด๐ฐ๐ฏ({ ๐ด๐ต๐ข๐ต๐ถ๐ด: '๐ด๐ถ๐ค๐ค๐ฆ๐ด๐ด', ๐ฅ๐ข๐ต๐ข: ๐ถ๐ด๐ฆ๐ณ });
}));
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ฅ๐ฆ๐ง๐ข๐ถ๐ญ๐ต ๐ณ๐ฐ๐ถ๐ต๐ฆ๐ณ;
No try/catch in the route. No next(err) calls manually. Any thrown error โ expected or not โ flows directly to your central error handler.
๐ฃ๐ฎ๐ฟ๐ ๐ฐ: ๐ช๐ถ๐ฟ๐ถ๐ป๐ด ๐ถ๐ ๐ฎ๐น๐น ๐๐ผ๐ด๐ฒ๐๐ต๐ฒ๐ฟ ๐ถ๐ป ๐๐ ๐ฝ๐ฟ๐ฒ๐๐
// ๐ด๐ณ๐ค/๐ข๐ฑ๐ฑ.๐ต๐ด
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต ๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด ๐ง๐ณ๐ฐ๐ฎ '๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐ณ๐ข๐ฏ๐ฅ๐ฐ๐ฎ๐๐๐๐ } ๐ง๐ณ๐ฐ๐ฎ '๐ค๐ณ๐บ๐ฑ๐ต๐ฐ';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต ๐ถ๐ด๐ฆ๐ณ๐๐ฐ๐ถ๐ต๐ฆ๐ด ๐ง๐ณ๐ฐ๐ฎ './๐ณ๐ฐ๐ถ๐ต๐ฆ๐ด/๐ถ๐ด๐ฆ๐ณ๐ด';
๐ช๐ฎ๐ฑ๐ฐ๐ณ๐ต { ๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ } ๐ง๐ณ๐ฐ๐ฎ './๐ฎ๐ช๐ฅ๐ฅ๐ญ๐ฆ๐ธ๐ข๐ณ๐ฆ/๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ';
๐ค๐ฐ๐ฏ๐ด๐ต ๐ข๐ฑ๐ฑ = ๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด();
๐ข๐ฑ๐ฑ.๐ถ๐ด๐ฆ(๐ฆ๐น๐ฑ๐ณ๐ฆ๐ด๐ด.๐ซ๐ด๐ฐ๐ฏ());
// ๐๐ต๐ต๐ข๐ค๐ฉ ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต ๐๐ ๐ต๐ฐ ๐ฆ๐ท๐ฆ๐ณ๐บ ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต
๐ข๐ฑ๐ฑ.๐ถ๐ด๐ฆ((๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด, ๐ฏ๐ฆ๐น๐ต) => {
๐ค๐ฐ๐ฏ๐ด๐ต ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ = (๐ณ๐ฆ๐ฒ.๐ฉ๐ฆ๐ข๐ฅ๐ฆ๐ณ๐ด['๐น-๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต-๐ช๐ฅ'] ๐ข๐ด ๐ด๐ต๐ณ๐ช๐ฏ๐จ) || ๐ณ๐ข๐ฏ๐ฅ๐ฐ๐ฎ๐๐๐๐();
๐ณ๐ฆ๐ฒ.๐ฉ๐ฆ๐ข๐ฅ๐ฆ๐ณ๐ด['๐น-๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต-๐ช๐ฅ'] = ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ;
๐ณ๐ฆ๐ด.๐ด๐ฆ๐ต๐๐ฆ๐ข๐ฅ๐ฆ๐ณ('๐น-๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต-๐ช๐ฅ', ๐ณ๐ฆ๐ฒ๐ถ๐ฆ๐ด๐ต๐๐ฅ);
๐ฏ๐ฆ๐น๐ต();
});
// ๐๐ฐ๐ถ๐ต๐ฆ๐ด
๐ข๐ฑ๐ฑ.๐ถ๐ด๐ฆ('/๐ข๐ฑ๐ช/๐ถ๐ด๐ฆ๐ณ๐ด', ๐ถ๐ด๐ฆ๐ณ๐๐ฐ๐ถ๐ต๐ฆ๐ด);
// 404 ๐ฉ๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ โ ๐ฎ๐ถ๐ด๐ต ๐ค๐ฐ๐ฎ๐ฆ ๐ข๐ง๐ต๐ฆ๐ณ ๐ข๐ญ๐ญ ๐ณ๐ฐ๐ถ๐ต๐ฆ๐ด
๐ข๐ฑ๐ฑ.๐ถ๐ด๐ฆ((๐ณ๐ฆ๐ฒ, ๐ณ๐ฆ๐ด, ๐ฏ๐ฆ๐น๐ต) => {
๐ฏ๐ฆ๐น๐ต(๐ฏ๐ฆ๐ธ ๐๐ฐ๐ต๐๐ฐ๐ถ๐ฏ๐ฅ๐๐ณ๐ณ๐ฐ๐ณ(๐๐ฐ๐ถ๐ต๐ฆ ${๐ณ๐ฆ๐ฒ.๐ฎ๐ฆ๐ต๐ฉ๐ฐ๐ฅ} ${๐ณ๐ฆ๐ฒ.๐ฑ๐ข๐ต๐ฉ}));
});
// ๐๐ฆ๐ฏ๐ต๐ณ๐ข๐ญ ๐ฆ๐ณ๐ณ๐ฐ๐ณ ๐ฉ๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ โ ๐ฎ๐ถ๐ด๐ต ๐ฃ๐ฆ ๐ญ๐ข๐ด๐ต, ๐ฎ๐ถ๐ด๐ต ๐ฉ๐ข๐ท๐ฆ 4 ๐ฑ๐ข๐ณ๐ข๐ฎ๐ด
๐ข๐ฑ๐ฑ.๐ถ๐ด๐ฆ(๐ฆ๐ณ๐ณ๐ฐ๐ณ๐๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ณ);
๐ฆ๐น๐ฑ๐ฐ๐ณ๐ต ๐ฅ๐ฆ๐ง๐ข๐ถ๐ญ๐ต ๐ข๐ฑ๐ฑ;
The order matters. The errorHandler must be registered last. The 404 handler must come after all your routes. The request ID middleware must come first so every log line has it.
๐ช๐ต๐ฎ๐ ๐๐ต๐ถ๐ ๐ด๐ถ๐๐ฒ๐ ๐๐ผ๐ ๐ถ๐ป ๐ฝ๐ฟ๐ผ๐ฑ๐๐ฐ๐๐ถ๐ผ๐ป
Every error in your API now has:
โข A typed code field clients can handle programmatically (NOT_FOUND, UNAUTHORIZED, VALIDATION_ERROR)
โข A clean message safe to show users
โข A requestId that ties logs across every layer
โข Full stack traces in logs for unexpected failures
โข Zero internal detail leaked to the outside world
When something breaks at 2am, you grep the requestId from the client's error response and pull the full picture from your logs in seconds โ not hours.
๐ ๐๐/
โโโ ๐๐๐๐๐๐ /
โ โโโ ๐ด๐๐๐ธ๐๐๐๐.๐ก๐ # ๐ก๐ฆ๐๐๐ ๐๐๐๐๐ ๐๐๐๐ ๐ ๐๐
โโโ ๐๐๐๐๐๐๐ค๐๐๐/
โ โโโ ๐๐๐๐๐๐ป๐๐๐๐๐๐.๐ก๐ # ๐๐๐๐ก๐๐๐ ๐๐๐๐๐ โ๐๐๐๐๐๐
โโโ ๐ข๐ก๐๐๐ /
โ โโโ ๐๐ ๐ฆ๐๐๐ป๐๐๐๐๐๐.๐ก๐ # ๐๐ ๐ฆ๐๐ ๐๐๐ข๐ก๐ ๐ค๐๐๐๐๐๐
โโโ ๐๐๐ข๐ก๐๐ /
โ โโโ ๐ข๐ ๐๐๐ .๐ก๐ # ๐๐๐๐๐ ๐๐๐ข๐ก๐๐ , ๐๐ ๐ก๐๐ฆ/๐๐๐ก๐โ
โโโ ๐๐๐.๐ก๐ # ๐ค๐๐๐๐ ๐ก๐๐๐๐กโ๐๐
๐ข๐ป๐ฒ ๐๐ต๐ถ๐ป๐ด ๐๐ผ ๐ฎ๐ฑ๐ฑ ๐ป๐ฒ๐ ๐
If you want to go further, add a process-level handler for truly unexpected crashes:
// ๐ด๐ณ๐ค/๐ด๐ฆ๐ณ๐ท๐ฆ๐ณ.๐ต๐ด
๐ฑ๐ณ๐ฐ๐ค๐ฆ๐ด๐ด.๐ฐ๐ฏ('๐ถ๐ฏ๐ค๐ข๐ถ๐จ๐ฉ๐ต๐๐น๐ค๐ฆ๐ฑ๐ต๐ช๐ฐ๐ฏ', (๐ฆ๐ณ๐ณ) => {
๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ.๐ง๐ข๐ต๐ข๐ญ({ ๐ฆ๐ณ๐ณ }, '๐๐ฏ๐ค๐ข๐ถ๐จ๐ฉ๐ต ๐ฆ๐น๐ค๐ฆ๐ฑ๐ต๐ช๐ฐ๐ฏ โ ๐ด๐ฉ๐ถ๐ต๐ต๐ช๐ฏ๐จ ๐ฅ๐ฐ๐ธ๐ฏ');
๐ฑ๐ณ๐ฐ๐ค๐ฆ๐ด๐ด.๐ฆ๐น๐ช๐ต(1);
});
๐ฑ๐ณ๐ฐ๐ค๐ฆ๐ด๐ด.๐ฐ๐ฏ('๐ถ๐ฏ๐ฉ๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ฅ๐๐ฆ๐ซ๐ฆ๐ค๐ต๐ช๐ฐ๐ฏ', (๐ณ๐ฆ๐ข๐ด๐ฐ๐ฏ) => {
๐ญ๐ฐ๐จ๐จ๐ฆ๐ณ.๐ง๐ข๐ต๐ข๐ญ({ ๐ณ๐ฆ๐ข๐ด๐ฐ๐ฏ }, '๐๐ฏ๐ฉ๐ข๐ฏ๐ฅ๐ญ๐ฆ๐ฅ ๐ณ๐ฆ๐ซ๐ฆ๐ค๐ต๐ช๐ฐ๐ฏ โ ๐ด๐ฉ๐ถ๐ต๐ต๐ช๐ฏ๐จ ๐ฅ๐ฐ๐ธ๐ฏ');
๐ฑ๐ณ๐ฐ๐ค๐ฆ๐ด๐ด.๐ฆ๐น๐ช๐ต(1);
});

Fail fast. Fail loud. Let your process manager (PM2, Docker, Kubernetes) restart the service. Never let your app limp along in a broken state.
This pattern takes about 20 minutes to wire up on a new project. It has saved me hours of debugging on every project since.
If you're building a REST API in Express + TypeScript and you don't have something like this in place โ start here before you add another feature.
๐ด๐๐ ๐ฆ๐๐ข ๐ข๐ ๐๐๐ ๐ ๐๐๐๐๐๐๐๐๐ก ๐๐๐๐๐ โ๐๐๐๐๐๐๐ ๐๐๐ก๐ก๐๐๐ ๐๐ ๐๐๐๐๐ข๐๐ก๐๐๐? ๐๐๐๐๐กโ๐๐๐ ๐ฆ๐๐ข'๐ ๐๐๐ ๐๐ ๐โ๐๐๐๐ โ๐๐๐? ๐ท๐๐๐ ๐๐ก ๐๐ ๐กโ๐ ๐๐๐๐๐๐๐ก๐ โ ๐ผ'๐ ๐๐๐๐ข๐๐๐๐๐ฆ ๐๐ข๐๐๐๐ข๐ ๐คโ๐๐ก ๐๐กโ๐๐๐ โ๐๐ฃ๐ ๐๐๐๐๐๐ ๐๐.
Top comments (0)