Cosa sono le JavaScript Promise e cos’è il chaining

Le Promise sono uno dei modi con cui possiamo gestire le asynchronous operation in ES6: .resolve() .reject() .then() .catch() and more!

Stefano Marchisio
7 min readAug 10, 2020

Introduzione

JavaScript è un linguaggio single-thread, per questo motivo siamo costretti a fare certe operazioni in modo asincrono per non bloccare la UI, un esempio fra tutti le operazioni Ajax. Infatti in tutti questi casi come parametro ad un metodo viene anche passata una funzione di callback, da invocare quando saranno disponibili i risultati. Le cose iniziano a diventare complicate quando dobbiamo svolgere più operazioni in modo sequenziale, infatti ciò comporta un annidamento dei vari callback rendendo alla lunga il codice difficile da leggere. Sotto un esempio di ciò.

firstRequest(function(response) {  secondRequest(response, function(nextResponse1) {    thirdRequest(nextResponse1, function(nextResponse2) {      console.log(‘Final response: ‘ + nextResponse2);    }, failureCallback);  }, failureCallback);}, failureCallback);

Con l’introduzione di es6 abbiamo a disposizione le “Promise” per poter effettuare questo tipo di operazioni. Il realtà le abbiamo anche in TypeScript, in questo caso però se mettiamo come target di compilazione es5, il compilatore le tradurrà in qualche cosa comprensibile anche ai browser più datati. Se invece scriviamo codice JavaScript puro, dobbiamo essere sicuri che poi il codice venga eseguito in un browser moderno.

Fatta questa premessa vediamo ora cosa sono le “Promise”. In linea di massima le “Promise” non fanno nulla di più di ciò che poteva essere fatto prima con i callback. Infatti il codice sovrastante può essere tradotto con seguente sintassi.

firstRequest()  .then(function(response) {    return secondRequest(response);  }).then(function(nextResponse1) {    return thirdRequest(nextResponse1);  }).then(function(nextResponse2) {    console.log(‘Final response: ‘ + nextResponse2);  }).catch(failureCallback);

Come si può vedere è molto più chiaro non avendo tutti quei callback annidati che creano confusione.

Che cosa è una Promise, e cosa sono resolve, reject?

Una Promise in JavaScript è simile a una promessa nella vita reale, una garanzia che faremo qualcosa in futuro. Inoltre una promessa può avere 2 possibili esiti: o sarà mantenuta quando verrà il momento oppure no. Lo stesso vale per le Promise in JavaScript, infatti questa potrà essere risolta (o Fulfilled) al momento opportuno oppure sarà rifiutata (o Rejected).

Una Promise in JavaScript è un oggetto che può avere 3 stati: Pending, Fulfilled, Rejected.

· Pending: è lo stato iniziale in attesa che la promessa sia Resolved o Rejected

· Fulfilled: la Promise è stata risolta con successo

· Rejected: la Promise è stata rifiutata o si è verificato un errore

Per esempio, quando richiediamo dei dati ad un server la “Promise” sarà nello stato di “Pending” finchè il server non risponderà, le caso poi il server abbia risposto positivamente lo stato diventerà “Fulfilled”, nel caso invece il server abbia risposto negativamente lo stato diventerà “Rejected”.

const myPromise = new Promise( (resolve, reject) => {  if (condition === ‘OK’)    resolve(‘Promise is resolved successfully.’);  else reject(‘Promise is rejected’);});

Quando creiamo una Promise nel costruttore dobbiamo passare una arrow function chiamata “executor”, questa funzione ha 2 parametri (resolved, rejected).

L’executor dovrebbe fare qualche cosa in modo asincrono e chiamare i metodi resolve() o reject() quando ha finito, anche se questo non è strettamente necessario. Infatti possiamo chiamare i metodi di callback (resolve, reject) anche subito.

Quando executor chiamerà i metodi di callback (resolve, reject) lo stato passerà da “pending” a “fulfilled” o “rejected”. I metodi di callback (resolve, reject) devono essere chiamati una volta sola, eventuali ulteriori chiamate verranno ignorate.

Promise chaining e gli operatori .then() .catch() .finally()

L’oggetto Promise funge da collegamento tra executor e le “funzioni di consumo” che possono essere registrate (o sottoscritte) usando i metodi: .then(), .catch(), .finally(). Questi metodi vengono infatti chiamati quando una promessa va a buon fine oppure viene rifiutata.

La particolarità di questi metodi è che ritornano a loro volta una Promise

In questo modo è possibile costruire una catena o chaining (un po’ come avviene con JQuery).

let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 3 * 100);
});
p.then((result) => {
console.log(result); // 10
return result * 2;
}).then((result) => {
console.log(result); // 20
return result * 3;
}).then((result) => {
console.log(result); // 60
return result * 4;
});
Output: 10, 20, 60

Se nel metodo .then() facciamo semplicemente return di qualche cosa, questo verrà automaticamente convertito in una nuova Promise già risolta. In questo modo possiamo concatenare un nuovo .then() con cui intercettare il valore ritornato.

Analogamente è possibile restituire una Promise ma in questo caso sarà nello stato “pending”, sarà poi lo sviluppatore che deciderà se risolverla o rifiutarla.

let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 3 * 100);
});
p.then((result) => {
console.log(result);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result * 2);
}, 3 * 1000);
});
}).then((result) => {
console.log(result);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result * 3);
}, 3 * 1000);
});
}).then(result => console.log(result));
Output: 10, 20, 60

Una cosa importante da notare è che i vari metodi .then() sono stati concatenati in sequenza ( visto che un metodo .then() ritorna una Promise ). Se invece si richiama il metodo .then() più volte sulla stessa Promise, non ci sarà un concatenamento delle Promise.

let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10);
}, 3 * 100);
});
p.then((result) => {
console.log(result); // 10
return result * 2;
})
p.then((result) => {
console.log(result); // 10
return result * 3;
})
p.then((result) => {
console.log(result); // 10
return result * 4;
});
Output: 10, 10, 10

Nell’esempio sovrastante avendo più gestori per una Promise, e non essendo i vari gestori in relazione tra loro, ogni gestore avrà un’esecuzione autonoma. Infatti non passano il risultato dall’uno all’altro come la Promise concatenata.

Il metodo .then() è possibile usarlo solo per le Promise risolte, se invece la Promise è stata rifiutata (o si è verificato un errore) dobbiamo usare il metodo .catch(). Il metodo .catch() può essere usato come il metodo then() ovvero concatenandolo dopo il metodo .then().

let p = new Promise((resolve, reject) => {
setTimeout(() => {
reject (‘Error, promise reject’);
}, 3 * 100);
});
p.then((result) => {
console.log(result); // 10
return result * 2;
}).then((result) => {
console.log(result); // 20
return result * 3;
}).then((result) => {
console.log(result); // 60
return result * 4;
}) .catch((message) => {
console.log(message);
});
Output: Error, promise reject

Se la Promise viene rifiutata, il controllo passerà direttamente al metodo .catch() e in questo caso vedremo un messaggio diverso sulla console.

Try … Catch implicito in una Promise ed errori

Il codice nella funzione “executor” ha un try … catch invisibile per questo motivo se si verifica un eccezione verrà rilevata e trattata come un rifiuto.

new Promise((resolve, reject) => {  throw new Error(“Whoops!”);}).catch(alert); // Error: Whoops!new Promise((resolve, reject) => {  reject(new Error(“Whoops!”));}).catch(alert); // Error: Whoops!

I 2 snippet di codice sovrastanti fanno la stessa cosa. Ma questo non accade solo nella funzione “executor”, ma anche nelle sue funzioni gestore. Se scateniamo un’eccezione all’interno di un metodo .then() questo fa si che la promessa venga rifiutata ed il controllo passa al gestore degli errori più vicino.

new Promise((resolve, reject) => {  resolve(“ok”);}).then((result) => {  throw new Error(“Whoops!”); // rejects the promise}).catch(alert); // Error: Whoops!

Come abbiamo notato un .catch() alla fine di una catena catena è simile a try … catch. Infatti possiamo avere tutti i gestori .then() che vogliamo e quindi utilizzare un solo .catch() alla fine della catena per gestire tutti gli errori.

Se scateniamo invece un eccezione all’interno di un metodo .catch() il controllo passa al gestore degli errori più vicino (successivo). Se gestiamo l’errore, allora l’elaborazione della catena continua fino al successivo .then().

Promise.all(iterable) e Promise.race(iterable)

Esitono dei casi in cui abbiamo una lista promise e per andare avanti abbiamo bisogno che tutte le Promise della lista vengano risolte o rifiutate, piuttosto che anche solo una Promise della lista venga risolta o rifiuta.

Per far ciò l’oggetto Promise mette a disposizione 2 metodi statici che ci permettono di fare ciò.

Promise.all(iterable) — Ritorna una promise che si risolve quando tutte le promises dell’argomento iterabile sono state risolte. Oppure, viene rigettato appena una Promise dell’argomento di tipo Iterable viene rigettata.

Promise.race(iterable) — Restituisce una promise che si risolve o respinge, non appena una delle promises dell’iterable si risolve o respinge.

Conclusioni

In questo modo la gestione di operazioni asincrone utilizzando le Promise risulta molto più chiara non avendo una serie di callback annidate. Ma con la versione ES7 sono state introdotte “async” e “await” che rende ancora più semplice la gestione di operazioni asincrone utilizzando le Promise.

IMPORTANTE: Quello scritto nel presente articolo con un browser vecchio non vale perchè le Promise non sono supportate. Per cui non rimane che fare le asynchronous operation alla vecchia maniera: ovvero con le callback!

Per approfondire: Cosa sono in JavaScript async e await e come cambia l’ uso delle Promise

Se volete contattarmi il mio profilo Linkedin è il seguente: Stefano Marchisio: Consulente freelance Angular ASP.NET MVC C#

--

--