La programmazione asincrona è un elemento molto comune nella programmazione in Nodejs e più in generale nel mondo Javascript. Solitamente si tratta di uno scenario piuttosto complesso, in quanto, soprattutto in questo caso, le idee sono spesso confuse e scrivere del codice "sporco" può portare ad una cattiva progettazione, scarsa manutenibilità e di conseguenza ad errori molto difficili da scovare e gestire. Per gestire la complessità della programmazione asincrona in Nodejs è necessario dunque munirsi di strumenti che vadano a supportare l'attività del programmatore e che siano atti al miglioramento della leggibilità del codice.

Le soluzioni sono molteplici ed a queste necessità rispondono molto bene le Promise, dei costrutti disponibili nativamente in Javascript che molto semplicemente inglobano del codice che verrà "eseguito in un futuro", quasi ci fosse appunto una promessa, un contratto tra due parti che stabilisce che "prima o poi" il codice asincrono verrà eseguito. 

Le Promise vengono create come dei comuni oggetti ed in seguito gestiti cc. La funzione che crea o riceve una Promise deve occuparsi di gestire i vari stati di quest'ultima, che possono essere:

  • pending (attesa), che corrisponde allo stato iniziale, quando la Promise non è ancora stata risolta o respinta
  • fulfilled (soddisfatta), significa che l'operazione asincrona si è conclusa con successo
  • rejected (respinto), significa che l'operazione asincrona è fallita per qualche ragione

Una Promise può essere risolta con un valore o rifiutata con una motivazione. Qualunque funzione prenda in carico una Promise è dunque esortata a chiamare nel momento opportuno i due metodi then e catch che incorporano rispettivamente la gestione dello stato fulfilled e rejected.

Il costruttore di una Promise accetta due funzioni anonime come parametri, la prima viene eseguita quando e se la Promise viene risolta mentre la seconda viene eseguita nel caso in cui la Promise venga respinta.

Un primo esempio semplice

Vediamo subito un esempio di un banale programma Nodejs che esemplifica quanto appena detto. Tutto il codice scritto in questo articolo utilizza lo standard ES6.

const sayHello = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve('Hello');
    }, 2000);
  });
}

let p = sayHello();

p.then((val) => {
  console.log(val);
}).catch((err) => {
  console.log(`An error occurred: ${err}`);
});

La funzione sayHello in questo caso crea e restituisce una Promise costruita con due funzioni anonime: la prima viene invocata dopo due secondi con la stringa "Hello" come parametro mentre la seconda in questo caso non viene invocata in quanto il codice in questione non restituisce degli errori. Il programma compie dunque i seguenti passi:

  • Invoca la funzione sayHello ed assegna alla variabile p la Promise che viene restituita
  • Chiama il metodo then di p passando come parametro una funzione anonima che come parametro conterrà il valore invocato nella chiamata resolve della funzione sayHello, in questo caso appunto "Hello"
  • Chiama il metodo catch di p passando come parametro una funzione anonima con un oggetto, presumibilmente un errore. In questo caso però questa funzione non verrà mai utilizzata in quanto la funzione sayHello non ha invocato la funzione reject della Promise creata

Un esempio più complesso

Supponiamo ora di voler scrivere un programma Nodejs che elenca e restituisce i dettagli pubblici di un utente qualsiasi sulla piattaforma Github. Quest'ultima mette a disposizione delle API pubbliche, tra cui quella a cui siamo interessati:

https://api.github.com/users/:username

Se copiamo e incolliamo questo indirizzo in una nuova pagina del browser ci verrà restituito un documento json con tutte le informazioni pubbliche dell'utente inserito come parametro. Ora, solitamente Nodejs utilizza la programmazione asincrona per gestire tutte le chiamate http verso l'esterno, ed in particolare esiste un modulo, request, che si occupa di gestire tutte le chiamate REST. Prima di tutto è necessario installare questo modulo ed eventualmente aggiungerlo alle dipendenze:

$ npm install request --save

Ora creiamo una funzione getUserDetails che restituirà una Promise la quale, una volta risolta, conterrà l'oggetto json in questione e, in caso di fallimento della chiamata, restituirà un errore una volta rifiutata.

'use strict';

const request = require('request');
const API_BASE_URL = 'https://api.github.com';

const getUser = (username) => {
  let options = {
    url: `${API_BASE_URL}/users/${username}`,
    headers: {
      'User-Agent': 'request'
    }
  };

  return new Promise((resolve, reject) => {
    // Do async job here
    console.log('Performing request...');
    request.get(options, (err, resp, body) => {
      console.log('Got response!');
      if (err) {
          reject(err);
      } else {
        resolve(JSON.parse(body));
      }
    });
  });
};

getUser('defra91').then((data) => {
  console.log(data);
}).catch((err) => {
  console.log(`An error occurred ${err}`);
});

console.log('I am synchronous!');

Eseguendo da riga di comando questo programma farà una chiamata alle API di github e dopo qualche decimo di secondo (dipende dalla rete) verrà restituita una risposta. Mentre si è in attesa della risposta il flusso del programma non è bloccato ma procede tranquillamente con tutte le ulteriori chiamate, mentre la gestione delle risposte dell'operazione asincrona sono delegate ai metodi then e catchIn questo caso l'output sarà il seguente:

Performing request...
I am synchronous!
Got response!
{ login: 'defra91',
  id: 5763318,
  avatar_url: 'https://avatars0.githubusercontent.com/u/5763318?v=4',
  gravatar_id: '',
  url: 'https://api.github.com/users/defra91',
  html_url: 'https://github.com/defra91',
  followers_url: 'https://api.github.com/users/defra91/followers',
  following_url: 'https://api.github.com/users/defra91/following{/other_user}',
  gists_url: 'https://api.github.com/users/defra91/gists{/gist_id}',
  starred_url: 'https://api.github.com/users/defra91/starred{/owner}{/repo}',
  subscriptions_url: 'https://api.github.com/users/defra91/subscriptions',
  organizations_url: 'https://api.github.com/users/defra91/orgs',
  repos_url: 'https://api.github.com/users/defra91/repos',
  events_url: 'https://api.github.com/users/defra91/events{/privacy}',
  received_events_url: 'https://api.github.com/users/defra91/received_events',
  type: 'User',
  site_admin: false,
  name: 'Luca De Franceschi',
  company: null,
  blog: '',
  location: 'Italia',
  email: null,
  hireable: true,
  bio: null,
  public_repos: 32,
  public_gists: 0,
  followers: 20,
  following: 19,
  created_at: '2013-10-24T07:36:19Z',
  updated_at: '2018-04-04T13:11:16Z' }

Sequenza di Promise

Supponiamo di voler ottenere gli stessi dati appena ottenuti ma non di un utente singolo, bensì di una lista di utenti e di voler mettere insieme i risultati in un array. In questo caso una via possibile è quella di invocare la funzione getUser tante volte quanti gli utenti in questione. Con il medesimo costrutto delle Promise è possibile dunque costruire una sequenza di Promise e gestire il then e catch solamente quando tutte le Promise sono state risolte. In questo caso una volta ottenuti gli utenti riduciamo il risultato in modo da ottenere solamente alcuni campi. Per fare questo dobbiamo servirci del metodo Promise.all

'use strict';

const request = require('request');
const API_BASE_URL = 'https://api.github.com';

const getUser = (username) => {
  let options = {
    url: `${API_BASE_URL}/users/${username}`,
    headers: {
      'User-Agent': 'request'
    }
  };

  return new Promise((resolve, reject) => {
    // Do async job here
    console.log('Performing request...');
    request.get(options, (err, resp, body) => {
      console.log('Got response!');
      if (err) {
          reject(err);
      } else {
        resolve(JSON.parse(body));
      }
    });
  });
};

const getMultipleUsers = (users) => {
  var promises = users.map((user) => {
    return getUser(user);
  });
  return Promise.all(promises);
};

const handleError = (err) => {
  console.log(`An error occurred ${err}`);
};

getMultipleUsers(['defra91', 'ngrx', 'angular']).then((result) => {
  let users = result.map((user) => {
    return { login: user.login, url: user.url, id: user.id };
  });
  console.log(users);
}).catch(handleError);

In questo caso l'output del programma sarà il seguente:

Performing request...
Performing request...
Performing request...
Got response!
Got response!
Got response!
[ { login: 'defra91',
    url: 'https://api.github.com/users/defra91',
    id: 5763318 },
  { login: 'ngrx',
    url: 'https://api.github.com/users/ngrx',
    id: 16272733 },
  { login: 'angular',
    url: 'https://api.github.com/users/angular',
    id: 139426 } ]

Vediamo nel dettaglio il funzionamento:

  • La funzione getMultipleUsers riceve in input un array di stringhe corrispondenti ai nomi degli utenti che vogliamo ottenere
  • Crea ed assegna alla variabile promises un'array di Promise, ciascuna restituita invocando la funzione getUser sul singolo nome utente
  • L'array di Promise viene dunque dato in pasto alla funzione Promise.all che si occupa di eseguire parallelamente le Promise ricevute
  • Promise.all a sua volta restituisce una Promise, la quale viene risolta solamente quando tutte le Promise ricevute sono state risolte
  • Qualora anche solo una Promise venisse rifiutata allora viene rifiutata immediatamente anche la promessa restituita da Promise.all, indipendentemente dal fatto che le altre Promise devono ancora essere risolte

Il risultato finale è un array contenente tutti gli utenti che sono stati richiesti.

Conclusioni

Il costrutto delle Promise aiuta sensibilmente lo sviluppatore nella gestione di funzionalità asincrone di un programma Nodejs. Ma la sua estensione va ovviamente oltre e si presta a moltissimi scenari in cui il linguaggio Javascript è protagonista. Si tratta di un'ottima variante alle callback, forse più pulita e leggibile, e, come si può evincere dall'esempio, può tranquillamente coesistere con esse. Con il crescere della complessità di un progetto Nodejs è impensabile non munirsi di strumenti robusti ed efficaci per la gestione di funzioni asincrone e le Promise sono ormai una realtà ben nota al mondo degli sviluppatori.

È possibile consultare o fare una pull-request del codice sorgente visitando il repository pubblico al seguente indirizzo:

https://github.com/defra91/nodejs-promise-example