JavaScript Promises e programmazione asincrona

JavaScript è uno dei peggiori linguaggi per la programmazione asincrona. Usare le JavaScript Promises, però, permette di ovviare ad alcune debolezze che...

Sviluppare in JavaScript è tutto risate e divertimento finché non capita una cosa come questa:

Ammettiamolo: JavaScript è uno dei peggiori linguaggi per la programmazione asincrona. La soluzione più semplice da implementare sono le ben note callbacks. Funzionano. Ma possiamo considerarle un hack. Sono fonte di inconsistenza nello sviluppo di APIs, non c’è alcuna garanzia che vengano invocate e il più delle volte portano a livelli di innesto tali da produrre la nota Pyramid of Doom. La cosa più grave, però, è l’assenza di return e throw. Se invochiamo un return o lanciamo un throw all’interno di una callback, nessuno ne otterrà il valore o ne riceverà l’errore. Questo perchè è stato generato uno stack di chiamate totalmente nuovo. Questo articolo vuole essere un’analisi del Promise pattern, dei suoi vantaggi e di come costruire una promessa secondo la specifica Promises/A+.

In principio fu la specifica

Partiamo da una definizione.

La promessa è un valore asincrono. Nel dettaglio, si tratta di un first-class object che ad un certo punto, nel futuro, assumerà un valore. Ma, al momento, è solo un oggetto.

Ipotizziamo di sviluppare una API che non restituisca immediatamente un valore. La soluzione più logica è quella di fornire una callback a cui poter passare quest’ultimo.
Questo stile è noto come Continuation Passing Style. Una funzione, nella logica del CPS, può accettare un argomento extra che ne rappresenta l’esplicita continuazione (la nostra callback appunto). La più semplice implementazione del Promise pattern è rappresentata da un un oggetto avente un metodo, “then“, che registra la callback. Rielaborando il precedente snippet, avremo quindi:

In questa estrema semplificazione, possiamo notare una debolezza: se la callback viene registrata più di un secondo dopo la costruzione della promessa, allora non verrà invocata. E’ qui che subentra il concetto di stato.

Gli stati delle Javascript Promises

Una promessa si troverà sempre in uno di questi tre stati:

  • pending, ovvero in attesa di un valore
  • fulfilled, e quindi soddisfatta
  • rejected, ossia respinta

Da specifica, fulfilled e rejected sono condizioni immutabili in cui la promessa non può più transitare verso altri stati. Inizialmente, una promessa si configura come irrisolta, e la callback viene registrata come pending observer. Quando questa viene soddisfatta, l’evento viene notificato all’observer e la callback viene eseguita.

L’arte del Differire

Facciamo un pò di astrazione e irrobustiamo la nostra utility introducendo il concetto di deferred. Questo è un oggetto dotato di due metodi: uno per registrare gli observers e l’altro per notificare gli observers della risoluzione della promessa.
Ma una promessa, non sempre arriva a una risoluzione e di conseguenza non venir soddisfatta. Il  metodo reject ci permetterà di rigettare la promise restituendone la relativa reason.

La nostra raw promise inizia a prender forma, ma alcune importanti considerazioni legate alla specifica sono d’obbligo:

  • Una promessa dovrebbe ritornare a sua volta una promessa. In tal modo sarà possibile concatenare più operazioni tra loro (chaining) per cui il valore ritornato da una callback sarà il valore che soddisfa la promessa ritornata. Allo stesso modo, l’errore lanciato dalla callback farà fallire la promessa ritornata. Dato che la promessa sa di essere soddisfatta o meno, può propagare errori senza invocare alcuna callback finchè un successivo error handler non viene incontrato.
  • Non dovrebbe essere possibile invocare il metodo resolve più di una volta. Sarebbe opportuno restituire eventualmente un errore o semplicemente ignorare successive risoluzioni.

Tenuti presenti questi punti, eccoci ritornare all’inizio della nostra analisi:
Abbiamo di nuovo uno stack!!

Then and then again

Ottenere un risultato da una callback, implica essere nel posto giusto al momento giusto. Agganciare event listener ad un evento già verificatosi, significa perderne il risultato. Sviluppare avendo la possibilità di trattare i valori in maniera temporalmente indipendente è uno dei vantaggi delle Promises, le quali consentono quindi di focalizzarsi nella risoluzioni di problemi piuttosto che dimenarsi nel controllo del flusso. In questo post è stato brevemente analizzato il core della specifica Promises/A+ e molto altro ci sarebbe da aggiungere. Le varie implementazioni offrono tantissime potenzialità e un buon punto di partenza è la scelta di una libreria. Quelle maggiormente Promises/A+ compliant sono:

Ho volontariamente escluso le jQuery Deferreds in quanto considerate una storpiatura del pattern per i seguenti motivi. In conclusione, occorre tener presente che le Promises non sono un sostituto di eventi o streams, ma si incastrano perfettamente tra i vari patterns utilizzati nell’ambito di una programmazione asincrona.


Filippo Mangione

Filippo si occupa di sviluppo web, e in Caffeina si occupa dello sviluppo tecnologico client-side. Ha accumulato esperienza, lavorando in diverse agenzie italiane su progetti di caratura internazionale, fino ad arrivare in Caffeina dove è il riferimento per le attività di Frontend development.