Esistono 6 metodi statici nella classe Promise
. Qui copriremo rapidamente il loro casi d’uso.
Promise.all
Diciamo che vogliamo eseguire molte promise in parallelo, e aspettare che siano tutte pronte.
Per esempio, scaricare da diversi URL in parallelo e processare il contenuto quando abbiamo finito con tutti.
Ecco a cosa serve Promise.all
.
La sintassi è:
let promise = Promise.all([...promises...]);
Promise.all
accetta un array di promise (tecnicamente si può usare qualsiasi iterabile, ma solitamente si usa un array) e ritorna una nuova promise.
La nuova promise si risolve quando tutte le promise elencate vengono risolte, e l’array dei loro risultati diventa il risultato finale.
Per esempio, il Promise.all
sotto si ferma (settles) dopo 3 secondi, ed il suo risultato è un array [1, 2, 3]
:
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 quando le promise sono pronte: ogni promise contribuisce con un membro dell'array
È da notare che l’ordine relativo rimane lo stesso. Anche se la prima promise prendesse il tempo più lungo per risolversi (resolve), il suo risultato sarà sempre il primo nell’array dei risultati.
Un trucco comune consiste nel mappare array of di dati da lavorare in un array di promise, per poi avvolgerli (to wrap) in Promise.all
.
Per esempio, se abbiamo un array di URL, possiamo scaricarli tutti così:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// mappiamo tutti gli url con la promise ritornata da fetch
let requests = urls.map(url => fetch(url));
/// Promise.all attende fino a quando tutti i job sono risolti (resolved)
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
Un esempio migliore in cui scarichiamo informazioni utente per un array di utenti di GitHub in base al loro nome (potremmo scaricare una matrice di merci in base ai rispettivi id, la logica è la stessa)
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// tutte le risposte sono pronte, possiamo mostrare i loro codici di stato HTTP
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // mostra 200 per ogni url
}
return responses;
})
// mappa l'array di risposte in un array di response.json() per leggere il loro contenuto
.then(responses => Promise.all(responses.map(r => r.json())))
// è stato fatto il parsing JSON di tutte le risposte JSON: "users" è l'array con i risultati
.then(users => users.forEach(user => alert(user.name)));
Se una qualsiasi delle promise è respinta (rejected), Promise.all
viene immediatamente respinta (rejects) con l’errore.
Per esempio:
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
Qui la seconda promise viene respinta (rejects) in due secondi. Questo porta al rigetto immediato di Promise.all
, così .catch
viene eseguito: l’errore del rigetto diventa il risultato di tutto Promise.all
.
Se una promise è respinta (rejects), Promise.all
è immediatamente respinto, dimenticando completamente delle altre nella lista. I loro risultati sono ignorati.
Per esempio, se ci sono molte chiamate fetch
, come nell’esempio sopra, ed una di esse fallisce, le altre continueranno ad essere eseguite, ma Promise.all
le ignorerà. Probabilmente poi si fermeranno (settle), ma il loro risultato sarà ignorato.
Promise.all
non fa niente per cancellarle, perché nelle promise non esiste il concetto di “cancellazione”. In un altro capitolo copriremo AbortController
il cui scopo è aiutarci ocn questo, but ma non è una pare delle API Promise.
Promise.all(...)
accetta oggetti non-promise in un iterable
Normalmente, Promise.all(...)
accetta un iterabile (nella maggior parte dei casi un array) di promises. Ma se uno qualsiasi di questi oggetti non è una promise, viene “avvolto” (wrapped) in Promise.resolve
.
Per esempio, qui i risultati sono [1, 2, 3]
:
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2, // trattato come Promise.resolve(2)
3 //trattato come Promise.resolve(3)
]).then(alert); // 1, 2, 3
Così siamo in grado di passare valori non-promise a Promise.all
quando conveniente.
Promise.allSettled
Promise.all
viene respinto interamente se una sola delle promise viene respinta. Questo è buono in alcuni casi, quando abbiamo bisogno di tutti i risultati per proseguire:
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // il metodo render ha bisogno di tutti i risultati
Promise.allSettled
aspetta che tutte le promise siano ferme (settled): anche se una viene respinta (rejects), aspetta per le altre. L’array risultante ha:
{status:"fulfilled", value:result}
per le risposte con successo,{status:"rejected", reason:error}
per gli errori.
Per esempio, ci piacerebbe scaricare le informazioni su diversi utenti. Anche se una richiesta fallisce, siamo interessati alle altre .
Usiamo Promise.allSettled
:
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
I risultati (results
) nella linea (*)
sopra saranno:
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
Così, per ogni promise otteniamo il suo stato e valore/ragione
.
Polyfill
Se il browser non supporta Promise.allSettled
, è facile usare un polyfill:
if (!Promise.allSettled) {
const rejectHandler = reason => ({ status: 'rejected', reason });
const resolveHandler = value => ({ status: 'fulfilled', value });
Promise.allSettled = function (promises) {
const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
return Promise.all(convertedPromises);
};
}
In questo codice, promises.map
prende i valori in ingresso, li “trasforma” in promises (nel caso in cui sia stata passata una non-promise) con p => Promise.resolve(p)
, e poi vi aggiunge il gestore (handler) .then
.
Il gestore handler “trasforma” un risultato positivo v
in {state:'fulfilled', value:v}
, ed un errore r
in {state:'rejected', reason:r}
. Questo è esattamente il formato di Promise.allSettled
.
Poi possiamo usare il risultato di Promise.allSettled
per ottenere i risultati di tutte le promise date, anche se alcune venissero respinte.
Promise.race
Similmente a Promise.all
, accetta un iterabile di promise, ma invece di attendere che siano tutte finite, aspetta per il primo risultato (o errore), e va avanti con quello.
La sintassi è:
let promise = Promise.race(iterable);
For instance, here the result will be 1
:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
Così, il primo risultato/errore diventa il risultato di tutto Promise.race
. Quando la prima promise ferma (settled) “vince la gara” (wins the race), tutti i risultati/errori successivi sono ignorati.
Promise.any
Similar to Promise.race
, but waits only for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with AggregateError
– a special error object that stores all promise errors in its errors
property.
The syntax is:
let promise = Promise.any(iterable);
For instance, here the result will be 1
:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise “wins the race”, all further results are ignored.
Here’s an example when all promises fail:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error
});
As you can see, error objects for failed promises are available in the errors
property of the AggregateError
object.
Promise.resolve/reject
I metodi Promise.resolve
e Promise.reject
vengono utilizzati raramente nel codice moderno, poiché la sintassi async/await
(che studieremo più avanti) li rende obsoleti.
Li studiamo per completezza e per quelli che non possono utilizzare async/await
per qualche ragione.
Promise.resolve
Promise.resolve(value)
risolve una Promise con il risultato value
.
Come nell’esempio:
let promise = new Promise(resolve => resolve(value));
Il metodo viene utilizzato per compatibilità, quando ci si aspetta che una funzione ritorni una promise.
Ad esempio, la funzione loadCached
sotto, analizza un URL e ne memorizza (sulla cache) il suo contenuto. Per le future chiamate allo stesso URL verrà immediatamente ritornato il contenuto dalla cache, ma utilizzando Promise.resolve
per renderlo una promise, in questo modo il valore ritornato sarà sempre una promise:
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
Possiamo scrivere loadCached(url).then(…)
, perché ci viene garantito che la funzione ritorni una promise. Possiamo sempre utilizzare .then
dopo loadCached
. Questo è lo scopo di Promise.resolve
nella riga (*)
.
Promise.reject
Promise.reject(error)
rifiuta una promise con error
.
Come nell’esempio:
let promise = new Promise((resolve, reject) => reject(error));
Nella pratica, questo metodo non viene quasi mai utilizzato.
Riepilogo
There are 6 static methods of Promise
class:
Promise.all(promises)
– aspetta che tutte le promise siano risolte e ritorna array un array dei loro risultati. Se una qualsiasi delle promise date viene respinta, allora diventa l’errore diPromise.all
, e tutti gli altri risultati sono ignorati.Promise.allSettled(promises)
(un nuovo metodo) – aspetta che tutte le promises vengano risolte o respinte e ritorna un array dei loro risultati come oggetti con questa forma:state
:'fulfilled'
or'rejected'
value
(se risolta ((fulfilled) oreason
(se respinta (rejected)).
Promise.race(promises)
– aspetta che la prima promise sia ferma (settle), ed il suo risultato/errore diventa il risultato.Promise.any(promises)
(metodo aggiunto di recente) – aspetta che la prima promise venga risolta e restituisca il risultato. Se tutte le promises vengono respinte,AggregateError
diventa l’errore diPromise.any
.Promise.resolve(value)
– crea una promise risolta (resolved) con il valore dato.Promise.reject(error)
– crea una promise respinta (rejected) con il valore dato.
Di tutti questi, il più comunemente utilizzato è Promise.all
.