Express gestisce gli errori con un middleware speciale che riceve quattro argomenti, (err, req, res, next), e viene registrato dopo tutte le route. Express gli manda ogni errore lanciato dentro una route, ogni errore passato a next(err) e, in Express 5, ogni promise rifiutata da un handler async. L'handler decide lo status code e invia una sola risposta di errore, così il resto del codice può limitarsi a lanciare errori.
GET /users/99 risponde 404 con {"error":"No user with id 99"}, e la Console mostra la riga dell'error handler. Invia GET /users/1 e la route risponde normalmente; l'error handler entra in gioco solo quando qualcosa va storto.
Gli errori che Express intercetta per te
Da una route si arriva all'error handler in tre modi, e in Express 5 funzionano tutti e tre allo stesso modo:
GET /async risponde 500 con {"error":"Rejected in an async route"}; GET /sync e GET /next finiscono nello stesso handler con i loro messaggi. Ora togli next dai parametri dell'error handler e invia di nuovo GET /sync: con tre parametri Express lo tratta come un middleware normale, lo salta e risponde con la sua pagina di errore HTML predefinita.
Errori async: Express 5 ed Express 4
La route /async funziona perché Express 5 intercetta la promise rifiutata. Express 4 non lo faceva: l'errore diventava un rejection non gestito e la richiesta restava appesa.
| Express 4 | Express 5 | |
|---|---|---|
throw in un handler sincrono | Intercettato | Intercettato |
next(err) | Intercettato | Intercettato |
Promise rifiutata da un handler async | Non intercettato, la richiesta resta appesa | Intercettato |
Errore in un setTimeout o in una callback di evento | Non intercettato, il processo crasha | Non intercettato, il processo crasha |
Con Express 4, avvolgi ogni handler async oppure installa il pacchetto 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);
}));
L'ultima riga è uguale in entrambe le versioni: Express non può vedere un errore lanciato in una callback che non ha chiamato lui. Trasforma le API a callback in promise (quasi tutto Node ha una versione a promise, come fs/promises) e usa await.
Un handler 404 ed errori JSON coerenti
Un'API è più comoda da usare quando tutti gli errori hanno la stessa forma. Dai agli errori uno status, trasforma le route non trovate in un errore 404 e lascia che un solo handler li formatti tutti:
GET /nope non corrisponde a nessuna route, quindi l'handler 404 la trasforma in un errore e la risposta è {"error":"Cannot GET /nope","status":404}. Invia GET /users/99 e l'errore lanciato dalla route prende la stessa forma. Invia POST /users con il body {"name": }: express.json() non riesce a fare il parsing e chiama next() con un errore il cui status è 400, quindi la risposta è {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.
Non esporre gli stack trace
I messaggi del tuo HttpError sono scritti per il client. Messaggi e stack di tutto il resto (un driver del database, un errore di battitura che lancia un TypeError) possono rivelare percorsi di file, query e versioni delle librerie. L'handler qui sopra invia Internal server error per ogni 5xx e scrive nel log del server l'errore vero. if (res.headersSent) return next(err); copre un errore che capita dopo che una parte della risposta è già stata inviata: in quel caso l'unica mossa sicura è lasciare che Express chiuda la connessione.
L'handler predefinito di Express segue NODE_ENV: mette lo stack trace nella sua pagina HTML a meno che NODE_ENV non sia production. Impostalo sul server di produzione (NODE_ENV=production node server.js), ma non affidarti a questo: un handler personalizzato che non invia mai err.stack è più sicuro.
Errori comuni
Registrare l'error handler prima delle route. Express passa gli errori solo in avanti, quindi un handler sopra una route non vede mai gli errori di quella route. Mettilo per ultimo, dopo l'handler 404.
Intercettare un errore e non farci niente. Un try/catch che si limita al log lascia la richiesta appesa. Nel catch chiama next(err), rilancia l'errore o invia una risposta.
Inviare due risposte. Lanciare un errore dopo res.json() fa sì che l'error handler provi a inviare di nuovo. Controlla res.headersSent, come nell'esempio.
Per come funzionano i middleware normali e next(), vedi i middleware di Express.
Domande frequenti
Posso avere più di un error handler in Express?
Sì. Vengono eseguiti in ordine, e ognuno invia una risposta oppure chiama next(err) per passare l'errore avanti. Una divisione comune è un handler che scrive il log e uno che invia la risposta.
Cosa succede se un error handler lancia un errore?
Express passa il nuovo errore all'error handler successivo e, se non ce n'è nessuno, al suo handler interno, che invia una pagina 500. Tieni gli error handler semplici, così non possono fallire a loro volta.
Quale status code deve usare una risposta di errore?
Usa un 4xx quando il client ha inviato qualcosa di sbagliato (400 input non valido, 401 non autenticato, 403 non autorizzato, 404 non trovato, 409 conflitto) e 500 quando è fallito il tuo codice. Non rispondere mai 200 con un errore nel body.
Come restituisco errori di validazione per più campi?
Rispondi 400 con una lista, per esempio { "errors": [{ "field": "email", "message": "is required" }] }. Con zod, trasforma result.error.issues in quella forma nell'error handler, così ogni route segnala l'input non valido nello stesso modo.