
"Uncaught (in promise)": perché il tuo errore async sparisce dalla console
La differenza tra un'eccezione lanciata e una promise rejection non gestita, perché un try/catch attorno non sempre basta, e come catturare correttamente gli errori async.
Qualcosa fallisce da qualche parte nel tuo codice async, e invece di uno stack trace pulito ottieni questo in console:
⛔ Errore
Uncaught (in promise) TypeError: Failed to fetch
Non è un bug del browser e non è un tipo diverso di errore — è esattamente lo stesso TypeError che otterresti sincronamente, solo riportato diversamente perché nessuno lo stava ascoltando. Capire perché succede è la chiave per correggerlo in modo affidabile.
Cosa significa davvero “unhandled rejection”
Una Promise può risolversi in due modi: si risolve (resolve), o viene rifiutata (reject). Se del codice chiama .catch() (o un await dentro un try/catch) su quella promise, il rifiuto è “gestito”. Se nessuno lo fa, nel momento in cui il motore JavaScript nota che la promise è stata rifiutata e nessuno ha mai collegato un handler, lo riporta come unhandled rejection — è esattamente ciò che significa “Uncaught (in promise)”.
fetch("/api/data"); // nessun .then, nessun .catch — se questo viene rifiutato, nulla lo cattura
È diverso da un semplice throw sincrono, che crasha immediatamente nel punto in cui accade. Una promise rejection può restare “non gestita” per un po’ — il motore deve aspettare un tick per vedere se stavi per aggiungere un .catch() — il che è parte del motivo per cui appare come un tipo distinto di errore in console.
Perché un try/catch attorno alla chiamata a volte non basta
Questa è la parte che confonde di più:
try {
fetchData(); // fetchData è una funzione async, chiamata senza await
} catch (error) {
console.error(error); // non gira mai
}
async function fetchData() {
const res = await fetch("/api/data");
return res.json();
}
fetchData() restituisce immediatamente una promise — il try/catch attorno cattura solo errori lanciati sincronamente durante la chiamata a fetchData, non errori che accadono più tardi, in modo asincrono, al suo interno. Senza await davanti alla chiamata, il blocco try è già finito nel momento in cui la promise potrebbe essere rifiutata.
try {
await fetchData(); // ora il try/catch aspetta davvero e può catturare il rifiuto
} catch (error) {
console.error(error);
}
La regola da ricordare: try/catch cattura il rifiuto di una promise solo se fai await su quella promise (o altrimenti concateni un .catch() su di essa) — avvolgere la sola chiamata non basta.
Catturare i rifiuti con .then()/.catch()
Se non usi async/await, l’equivalente è collegare .catch() direttamente alla catena di promise:
fetchData()
.then((data) => render(data))
.catch((error) => console.error("Caricamento dati fallito:", error));
Un .catch() alla fine della catena cattura un rifiuto proveniente da qualsiasi step precedente — la chiamata iniziale fetchData() o qualsiasi .then() intermedio — uno dei motivi per cui .then()/.catch() concatenati sono spesso più facili da seguire rispetto a diversi blocchi try/catch separati sparsi nel codice async.
La rete di sicurezza globale: unhandledrejection
Per i casi che genuinamente non puoi prevedere (una chiamata async fire-and-forget da qualche parte in profondità in una libreria, per esempio), il browser ti offre un evento globale come ultima risorsa:
window.addEventListener("unhandledrejection", (event) => {
console.error("Promise rejection non gestita:", event.reason);
// event.preventDefault() evita che venga anche loggata in console come errore
});
⚠ Non sostituisce la gestione degli errori dove accadono
Considera unhandledrejection come una rete di sicurezza per logging/monitoring (Sentry e
strumenti simili si agganciano lì), non come la tua strategia principale di gestione errori.
Catturare gli errori vicino al punto in cui accadono — con un .catch() specifico o un
try/catch attorno all’await — è ciò che ti permette di recuperare davvero con grazia
(mostrare un pulsante di retry, usare dati in cache) invece di sapere solo che qualcosa si è
rotto.
FAQ
❓ Succede solo con fetch?
No — qualsiasi promise rifiutata senza handler lo scatena, che il rifiuto arrivi da fetch, una
chiamata al database, Promise.reject() direttamente, o una funzione async che lancia un
errore. fetch compare spesso semplicemente perché le chiamate di rete sono una fonte comune di
fallimenti reali.
❓ Una unhandled rejection manda in crash la mia app?
Nel browser, no — viene loggata come errore ma l’esecuzione continua. In Node.js, dalla v15, le promise rejection non gestite terminano il processo per default (un cambiamento deliberato rispetto alle versioni precedenti, che si limitavano ad avvisare), quindi vale la pena gestirle esplicitamente nel codice server invece di affidarsi al fatto che il processo resti vivo.
❓ AbortController è correlato a questo?
Correlato ma separato — annullare una fetch tramite AbortController fa sì che la promise venga
rifiutata con un AbortError, che comunque ha bisogno di un .catch()/try-catch come
qualsiasi altro rifiuto, altrimenti vedrai esattamente questo warning ogni volta che annulli una
richiesta. Tratto il pattern completo nell’articolo su
AbortController.
Conclusione
“Uncaught (in promise)” non è una categoria speciale di bug — è la console che ti dice che una promise rifiutata non ha mai avuto un .catch() o un await dentro un try/catch. La correzione è sempre la stessa: assicurati che ogni promise che crei o chiami sia o attesa (await) dentro un try/catch, concatenata con .catch(), o deliberatamente monitorata globalmente come ultima risorsa — non lasciata a fallire in silenzio.
Riferimenti

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.