Express gère les erreurs avec un middleware spécial qui prend quatre arguments, (err, req, res, next), et qui est enregistré après toutes vos routes. Express lui envoie chaque erreur levée dans une route, chaque erreur passée à next(err) et, dans Express 5, chaque promesse rejetée d'un handler async. Le gestionnaire choisit le code de statut et envoie une seule réponse d'erreur, donc le reste de votre code peut se contenter de lever des erreurs.
GET /users/99 répond 404 avec {"error":"No user with id 99"}, et la Console affiche la ligne du gestionnaire d'erreurs. Envoyez GET /users/1 et la route répond normalement ; le gestionnaire d'erreurs ne s'exécute que quand quelque chose se passe mal.
Les erreurs qu'Express intercepte pour vous
Une route a trois façons d'atteindre le gestionnaire d'erreurs, et dans Express 5 les trois fonctionnent de la même manière :
GET /async répond 500 avec {"error":"Rejected in an async route"} ; GET /sync et GET /next finissent dans le même gestionnaire avec leur propre message. Retirez maintenant next des paramètres du gestionnaire d'erreurs et renvoyez GET /sync : avec trois paramètres, Express le traite comme un middleware ordinaire, le saute et répond avec sa page d'erreur HTML par défaut.
Erreurs async : Express 5 et Express 4
La route /async fonctionne parce qu'Express 5 intercepte la promesse rejetée. Express 4 ne le faisait pas : l'erreur devenait un rejet non géré et la requête restait bloquée.
| Express 4 | Express 5 | |
|---|---|---|
throw dans un handler synchrone | Interceptée | Interceptée |
next(err) | Interceptée | Interceptée |
Promesse rejetée d'un handler async | Non interceptée, la requête reste bloquée | Interceptée |
Erreur dans un setTimeout ou un callback d'événement | Non interceptée, le processus plante | Non interceptée, le processus plante |
Avec Express 4, enveloppez chaque handler async ou installez le package express-async-errors :
// Express 4 only
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
res.json(user);
}));
La dernière ligne est la même dans les deux versions : Express ne peut pas voir une erreur levée dans un callback qu'il n'a pas appelé. Transformez les API à callbacks en promesses (la plupart des modules de Node ont une version à promesses, comme fs/promises) et utilisez await.
Un handler 404 et des erreurs JSON cohérentes
Une API est plus simple à utiliser quand toutes les erreurs ont la même forme. Donnez un status aux erreurs, transformez les routes non trouvées en erreur 404, et laissez un seul gestionnaire les mettre en forme :
GET /nope ne correspond à aucune route, donc le handler 404 la transforme en erreur et la réponse est {"error":"Cannot GET /nope","status":404}. Envoyez GET /users/99 et l'erreur levée par la route prend la même forme. Envoyez POST /users avec le corps {"name": } : express.json() n'arrive pas à le parser et appelle next() avec une erreur dont le status vaut 400, donc la réponse est {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.
Ne divulguez pas les stack traces
Les messages de votre propre HttpError sont écrits pour le client. Les messages et les stacks de tout le reste (un driver de base de données, une faute de frappe qui lève une TypeError) peuvent révéler des chemins de fichiers, des requêtes et des versions de bibliothèques. Le gestionnaire ci-dessus envoie Internal server error pour chaque 5xx et journalise la vraie erreur côté serveur. if (res.headersSent) return next(err); couvre une erreur qui survient après qu'une partie de la réponse a déjà été envoyée : la seule chose sûre à faire est alors de laisser Express fermer la connexion.
Le gestionnaire par défaut d'Express suit NODE_ENV : il inclut la stack trace dans sa page HTML sauf si NODE_ENV vaut production. Définissez-la sur votre serveur de production (NODE_ENV=production node server.js), mais ne comptez pas dessus : un gestionnaire personnalisé qui n'envoie jamais err.stack est plus sûr.
Erreurs fréquentes
Enregistrer le gestionnaire d'erreurs avant les routes. Express ne fait passer les erreurs que vers l'avant, donc un gestionnaire placé au-dessus d'une route ne voit jamais les erreurs de cette route. Mettez-le en dernier, après le handler 404.
Intercepter une erreur sans rien en faire. Un try/catch qui ne fait que journaliser laisse la requête en suspens. Dans le catch, appelez next(err), relancez l'erreur ou envoyez une réponse.
Envoyer deux réponses. Lever une erreur après res.json() pousse le gestionnaire d'erreurs à essayer d'envoyer à nouveau. Vérifiez res.headersSent, comme dans l'exemple.
Pour le fonctionnement des middlewares ordinaires et de next(), voir les middlewares Express.
Questions fréquentes
Peut-on avoir plusieurs gestionnaires d'erreurs dans Express ?
Oui. Ils s'exécutent dans l'ordre, et chacun envoie une réponse ou appelle next(err) pour transmettre l'erreur. Un découpage courant : un gestionnaire qui journalise et un autre qui envoie la réponse.
Que se passe-t-il si un gestionnaire d'erreurs lève une erreur ?
Express transmet la nouvelle erreur au gestionnaire d'erreurs suivant, et s'il n'y en a pas, à son gestionnaire intégré, qui envoie une page 500. Gardez les gestionnaires d'erreurs simples pour qu'ils ne puissent pas échouer eux-mêmes.
Quel code de statut utiliser pour une réponse d'erreur ?
Utilisez un 4xx quand le client a envoyé quelque chose d'incorrect (400 entrée invalide, 401 non connecté, 403 non autorisé, 404 introuvable, 409 conflit) et 500 quand votre code a échoué. Ne répondez jamais 200 avec une erreur dans le corps.
Comment renvoyer des erreurs de validation pour plusieurs champs ?
Répondez 400 avec une liste, par exemple { "errors": [{ "field": "email", "message": "is required" }] }. Avec zod, transformez result.error.issues en cette forme dans le gestionnaire d'erreurs pour que toutes les routes signalent une entrée invalide de la même façon.