Javascript. Il costrutto Async/Await
Una delle più attese novità introdotte da ECMAScript 2017 è stato il supporto di Async/Await.
Si tratta di due parole chiave che abilitano la gestione di funzioni asincrone eseguite tramite un approccio sincrono. Per comprendere l’utilità di questo nuovo costrutto, occorre innanzitutto capire quali siano gli approcci generalmente più utilizzati per l’esecuzione di operazioni asincrone in JavaScript. Possiamo individuarne due:
- le funzioni di callback, cioè funzioni “passate” come parametri di altre funzioni da eseguire al termine di una operazione asincrona;
- le Promise, cioè oggetti il cui stato rappresenta l’esecuzione di una attività asincrona.
L’introduzione delle Promise in JavaScript ha consentito di semplificare notevolmente la struttura del codice rispetto all’approccio basato sull’utilizzo di callback.
Il costrutto Async/Await si propone di andare oltre, consentendo la scrittura di codice asincrono pur mantenendo una struttura di codice tipico della programmazione sincrona, in modo analogo a come avviene in altri linguaggi di programmazione (quali Ruby e Java).
Ma vediamo nel dettaglio le caratteristiche di ciascuna parola chiave.
Innanzitutto, esse semplificano la sintassi per la gestione di operazioni asincrone; infatti, Async e Await si basano sul meccanismo delle Promise e il loro risultato è compatibile con qualsiasi API che le utilizza.
In particolare, la parola chiave async consente di dichiarare una funzione come asincrona (cioè che contiene un’operazione asincrona), mentre await sospende l’esecuzione di una funzione, in attesa che la Promise associata ad un’attività asincrona venga risolta o rigettata.
Per chiarire il concetto, consideriamo la seguente funzione.
function getArticolo(articoloId) {
fetch("/articoli/" + articoloId).then(response => {
console.log(response);
}).catch(error => console.log("Si è verificato un errore!"));
}
Essa utilizza la funzione fetch() per effettuare una chiamata HTTP (e quindi una operazione asincrona) e visualizzare sulla console i dati di un utente oppure un messaggio d’errore.
Possiamo riscrivere la funzione utilizzando async e await come mostrato di seguito:
async function getArticolo(articoloId) {
try {
let response = await fetch("/articoli/" + articoloId);
console.log(response);
} catch (e) {
console.log("Si è verificato un errore!");
}
}
Si nota bene che abbiamo premesso, alla dichiarazione della funzione getArticolo(), la parola chiave async per indicare che all’interno di essa verrà eseguita una operazione asincrona. Il codice contenuto nel corpo della funzione mantiene la struttura tipica di un normale codice sincrono. Troviamo infatti il blocco try/catch per intercettare le eventuali eccezioni ed una chiamata a fetch() come se si trattasse di una normale funzione sincrona. L’unica differenza consiste nella presenza di await davanti all’invocazione di fetch(). Questo approccio fa sì che l’esecuzione della funzione getArticolo() venga sospesa all’avvio dell’operazione asincrona e venga poi automaticamente ripresa non appena si ottiene un risultato, cioè quando la Promise associata a fetch() viene risolta o rigettata.
Questo semplice esempio è in grado di evidenziare in poche righe gli eccezionali vantaggi introdotti dalla coppia di parole chiave async e await in JavaScript: l’utilizzo della struttura sincrona del codice per gestire operazioni asincrone e l’uso di try/catch per intercettare le eventuali eccezioni.
Da notare bene che la parola chiave await può essere usata soltanto all’interno di funzioni marcate con async.
Tornando all’esempio mostrato poc’anzi, il risultato finale delle due versioni della funzione getArticolo() è analogo, anche se le modalità di esecuzione sono leggermente diverse.
È più chiaro studiando la seguente funzione:
async function getBlogAndPhoto(userId) {
try {
let utente = await fetch("/utente/" + userId);
let blog = await fetch("/blog/" + utente.blogId);
let foto = await fetch("/photo/" + utente.albumId);
return {
utente,
blog,
foto
};
} catch (e) {
console.log("Si è verificato un errore!");
}
}
Essa carica i dati dell’utente, poi i dati del blog associato allo stesso e quindi le sue foto; infine, la funzione restituisce un oggetto con tutte le informazioni recuperate.
Ciascuna operazione asincrona, “scatenata” dall’invocazione a fetch(), viene eseguita dopo il completamento della precedente invocazione. In altre parole, le operazioni asincrone non avvengono in parallelo, avendo quindi un potenziale impatto sulle prestazioni dell’applicazione.
Se volessimo trarre beneficio dall’esecuzione parallela delle chiamate HTTP, dovremmo utilizzare il metodo Promise.all(), come mostrato dal seguente codice:
async function getBlogAndPhoto(userId) {
try {
let utente = await fetch("/utente/" + userId);
let result = await Promise.all([
fetch("/blog/" + utente.blogId),
fetch("/photo/" + utente.albumId)
]);
return {
utente,
blog: result[0],
foto: result[1]
};
} catch (e) {
console.log("Si è verificato un errore!")
}
}
In questo caso, attendiamo il completamento del caricamento dei dati dell’utente, requisito essenziale per recuperare le altre informazioni, e quindi rimaniamo in attesa del caricamento in parallelo dei dati del blog e delle foto.
In conclusione, le parole chiave async e await ci aiutano a semplificare il codice per la gestione delle operazioni asincrone, ma non sostituiscono l’utilizzo delle Promise. Queste infatti continuano ad essere alla base dell’esecuzione di codice asincrono e in alcune situazioni risultano ancora insostituibili.