All three frameworks converge on the same idea for errors — a special layer (or hook) that only runs when something goes wrong — but the mechanism used to route control there differs meaningfully:
// ASP.NET Core — a normal try/catch around await next(), or the built-in helper:
app.UseExceptionHandler("/error");
// Express — a synchronous throw inside a normal handler is caught automatically;
// an async rejection must be forwarded explicitly via next(err)
app.get("/orders/:id", async (req, res, next) => {
try {
const order = await fetchOrder(req.params.id);
res.json(order);
} catch (err) {
next(err); // required — Express does NOT auto-catch promise rejections
}
});
# Django — process_exception, or letting DEBUG=False render a generic 500 page
class ErrorLoggingMiddleware:
def process_exception(self, request, exception):
log.exception("Unhandled error")
return None # None means "keep propagating" — don't swallow it here
The Express case is worth calling out specifically because it's a well-known gotcha: Express's automatic error catching only covers synchronous throws inside a plain handler — a rejected Promise inside an async route handler is not automatically forwarded to error-handling middleware unless the code explicitly catches it and calls next(err). An unhandled rejection there can crash the process or hang the request, depending on the Node/Express version, rather than producing a clean 500 response.