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 queste nuove parole chiave, occorre innanzitutto capire quali sono 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 lo stato di esecuzione di una attività asincrona.
L'introduzione delle Promise in JavaScript (di cui parleremo più avanti nel corso di questa guida) ha consentito di semplificare notevolmente la struttura del codice rispetto all'approccio basato sull'utilizzo di callback.
La coppia di parole chiave async/await di 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 C# e Python).
Ma vediamo nel dettaglio cosa ci offrono queste nuove parole chiave.
Iniziamo col dire che esse non propongono un nuovo meccanismo di gestione di operazioni asincrone, ma ne semplificano la sintassi. Infatti, async
e await
si basano sul meccanismo delle Promise e il loro risultato è compatibile con qualsiasi API che utilizza le Promise.
In particolare, la parola chiave async
consente di dichiarare una funzione come asincrona, cioè che contiene un'operazione asincrona, mentre la parola chiave 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 getUtente(userId) {
fetch("/utente/" + userId).then(response => {
console.log(response);
}).catch(error => console.log("Si è verificato un errore!"));
}
Essa utilizza 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 getUtente(userId) {
try {
let response = await fetch("/utente/" + userId);
console.log(response);
} catch (e) {
console.log("Si è verificato un errore!");
}
}
Come possiamo vedere, abbiamo premesso alla dichiarazione della funzione getUtente()
la parola chiave async
per indicare che all'interno di essa verrà eseguita una operazione asincrona. Il codice contentuto 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 accorgimento fa in modo che l'esecuzione della funzione getUtente()
venga sospesa all'avvio dell'operazione asincrona e venga poi automaticamente ripresa quando viene ottenuto 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.
Sottolineiamo che la parola chiave await può essere usata soltanto all'interno di funzioni marcate con async
.
Tornando all'esempio visto prima, il risultato finale delle due versioni della funzione getUtente()
è analogo, anche se le modalità di esecuzione sono leggermente diverse.
Possiamo rendercene conto prendendo in considerazione 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 all'utente e quindi le foto associate all'utente. 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 si sostituiscono all'utilizzo delle Promise. Queste infatti continuano ad essere alla base dell'esecuzione di codice asincrono e in alcune situazioni risultano ancora insostituibili. Per approfondire l'argomento relativo alla Promise, rimandiamo all'apposita lezione di questa guida.