Menu

Promise in JavaScript: then, catch e Promise.all spiegati

Come funzionano le Promise in JavaScript: i tre stati, la concatenazione con then e catch, la combinazione con Promise.all e come crearne una con new Promise.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Una Promise è un segnaposto per un valore futuro

Quando JavaScript deve fare qualcosa che richiede tempo (una richiesta di rete, la lettura di un file, l'attesa di un timer), non può darti subito il risultato. Ti dà invece una Promise: un oggetto che rappresenta un valore che esisterà, prima o poi.

Il primo console.log mostra una Promise in attesa. Mezzo secondo dopo la Promise si risolve, e la callback di .then viene eseguita con il valore. La Promise in sé è solo un oggetto; la magia sta nel fatto che sa avvisare chiunque sia in ascolto quando il suo valore arriva.

I tre stati

Una Promise si trova sempre in uno di tre stati:

  • pending: il lavoro è in corso. Ancora nessun valore.
  • fulfilled: il lavoro è riuscito. C'è un valore disponibile.
  • rejected: il lavoro è fallito. C'è un errore disponibile.

Una Promise passa da pending a fulfilled oppure a rejected esattamente una volta, e poi resta lì per sempre. Non puoi annullare la risoluzione di una Promise né risolverla due volte.

Promise.resolve(value) costruisce una Promise già risolta; Promise.reject(error) ne costruisce una già rifiutata. Sono comode per i test e per restituire una Promise da una funzione che a volte ha già la risposta pronta.

Leggere il valore: .then e .catch

Non estrai direttamente il valore da una Promise: passi una callback a .then, e la Promise la chiama quando il valore è pronto:

.catch(fn) viene eseguito se la Promise viene rifiutata. Dietro le quinte è una scorciatoia per .then(undefined, fn). Un .catch() alla fine di una catena gestisce i rifiuti di qualsiasi passaggio precedente: non te ne serve uno dopo ogni .then.

Concatenare: ogni .then restituisce una nuova Promise

Questa è la parte che confonde molti. .then() non si limita a eseguire una callback: restituisce una nuova Promise che si risolve con qualunque cosa la callback abbia restituito. Questo ti permette di concatenare:

Ogni passaggio alimenta il successivo. Se una callback di .then restituisce una Promise, la catena aspetta quella Promise prima di continuare, così i passaggi asincroni si compongono senza intoppi:

Tre passaggi asincroni in sequenza, senza annidamenti. Confrontalo con la stessa logica scritta con le callback e capirai perché le Promise hanno avuto successo.

Gli errori scendono lungo la catena

Una Promise rifiutata salta ogni .then finché non trova un .catch. Questo è l'intero modello di gestione degli errori:

Lanciare un errore dentro un .then rifiuta la Promise restituita da quel .then. Il .then successivo vede il rifiuto e lo passa avanti, finché .catch non lo intercetta. Di solito basta un solo .catch alla fine della catena, e una catena senza nessun .catch produce un avviso di "unhandled promise rejection", che conviene sistemare.

Creare la tua con new Promise

La maggior parte delle volte userai Promise che ti forniscono le librerie. Ogni tanto però devi avvolgere qualcosa che non ne restituisce una, di solito una vecchia API basata su callback:

La funzione che passi a new Promise si chiama executor. Riceve due argomenti: resolve (chiamalo con il valore di successo) e reject (chiamalo con un errore). Chiamane uno esattamente una volta. Dopo, le chiamate successive vengono ignorate.

Due abitudini che ti risparmiano problemi:

  • Usa new Promise solo per avvolgere qualcosa che non è già basato sulle Promise. Se una funzione restituisce già una Promise, restituiscila e basta.
  • Chiama sempre reject con un oggetto Error, non con una stringa. Vale la pena conservare lo stack trace.

Eseguire operazioni in parallelo: Promise.all

Le catene di .then vengono eseguite in sequenza. Quando hai diverse operazioni asincrone indipendenti e vuoi eseguirle contemporaneamente, lo strumento giusto è Promise.all:

I tre timer vengono eseguiti in parallelo. Promise.all si risolve con un array di risultati nello stesso ordine dell'input, una volta che ogni Promise è stata risolta. Il tempo totale è di circa 400ms, non 900ms.

Il rovescio della medaglia: Promise.all viene rifiutata appena una qualsiasi delle sue Promise viene rifiutata, e gli altri risultati vanno persi. È il comportamento giusto quando ti servono tutti i pezzi (per esempio, per mostrare una pagina che richiede tre chiamate API). Quando non è così, usa allSettled.

Quando qualche fallimento va bene: Promise.allSettled

Promise.allSettled aspetta che ogni Promise finisca, risolta o rifiutata, e ti restituisce un resoconto:

Ogni risultato è un oggetto: { status: "fulfilled", value } oppure { status: "rejected", reason }. È utile quando un successo parziale è accettabile: registrare un gruppo di eventi, scaricare una serie di miniature, eseguire controlli di stato indipendenti.

Altri due combinatori da conoscere:

  • Promise.race([...]): si conclude appena la prima Promise si conclude, in un senso o nell'altro. Comodo per i timeout.
  • Promise.any([...]): si risolve con il primo successo e ignora i rifiuti. Viene rifiutata solo se tutte le Promise vengono rifiutate.

Le Promise sono sempre asincrone

Anche una Promise già risolta chiama la sua callback di .then in modo asincrono: mai in modo sincrono, mai nello stesso tick:

L'output è prima, dopo, immediato. La callback di .then aspetta che il codice corrente finisca, poi viene eseguita nella coda dei microtask. Questa regola ("la callback di una Promise non viene mai eseguita in modo sincrono") è il motivo per cui mescolare Promise e codice sincrono è prevedibile: il codice sincrono finisce sempre per primo.

Prossimo passo: async/await

Concatenare chiamate a .then funziona, ma quando i passaggi sono più di due o tre inizia a sembrare una scalinata. async/await è sintassi costruita sopra le Promise che ti permette di scrivere la stessa logica come se fosse sincrona, con try/catch per gli errori e normali variabili per i valori intermedi. È l'argomento della prossima pagina.

Domande frequenti

Cos'è una Promise in JavaScript?

Una Promise è un oggetto che rappresenta un valore non ancora pronto, di solito il risultato futuro di un'operazione asincrona come una richiesta di rete. Si trova sempre in uno di tre stati: pending, fulfilled o rejected. Leggi il valore finale collegando delle callback con .then() e .catch().

Qual è la differenza tra then e catch?

.then(onFulfilled) viene eseguito quando la Promise si risolve con successo e riceve il valore risolto. .catch(onRejected) viene eseguito quando la Promise (o una qualsiasi Promise precedente nella catena) viene rifiutata e riceve l'errore. Un solo .catch() alla fine di una catena gestisce i fallimenti di qualsiasi passaggio precedente.

Cosa fa Promise.all?

Promise.all([p1, p2, p3]) riceve un array di Promise e restituisce una sola Promise che si risolve con un array di tutti i valori risolti, ma solo quando ogni Promise in ingresso è stata risolta. Se anche solo una viene rifiutata, l'intera operazione viene rifiutata subito. Usa Promise.allSettled quando vuoi tutti i risultati a prescindere dai fallimenti.

Meglio usare le Promise o async/await?

Sono lo stesso meccanismo: async/await è sintassi costruita sopra le Promise. La maggior parte del codice nuovo si legge meglio con async/await, ma continui a restituire Promise, a catturare gli errori con try/catch o .catch() e a usare Promise.all per eseguire operazioni in parallelo. Sapere come funzionano le Promise dà senso ad async/await.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA