JavaScript è un linguaggio single-threaded che viene eseguito in un unico thread. Questo significa che, se eseguisse tutte le operazioni in modo strettamente sequenziale (sincrono), il codice rischierebbe di bloccarsi per operazioni lente (come richieste di rete o letture da file).
Per risolvere questo problema, JavaScript adotta un modello di concorrenza basato sugli event loop e sulle callback. Questo meccanismo gli consente di delegare alcune operazioni a thread “esterni” (come quelli del browser o del sistema), mentre l’esecuzione principale prosegue senza bloccare l’utente.
L’evoluzione del codice asincrono: dalle callback alle Promesse
Le Callback
In passato, la gestione del codice asincrono si basava su funzioni di callback, ossia funzioni passate come parametro ad altre funzioni, che venivano eseguite al completamento di una determinata operazione. Tuttavia, ciò portava facilmente al fenomeno noto come callback hell:
operazione1(function(risultato1) {
operazione2(risultato1, function(risultato2) {
operazione3(risultato2, function(risultato3) {
// Ecc...
});
});
});
La leggibilità del codice ne risentiva, così come la gestione degli errori, che diventava complicata.
Le Promesse
Per risolvere queste criticità, JavaScript (dalla specifica ECMAScript 2015) ha introdotto le Promises. Una promessa rappresenta il risultato di un’operazione asincrona, e può trovarsi in uno di tre stati:
- Pending (in attesa): la promessa è in corso di esecuzione, non ha ancora restituito un risultato né un errore.
- Fulfilled (risolta con successo): l’operazione asincrona è terminata con esito positivo.
- Rejected (rifiutata): l’operazione è terminata con errore o esito negativo.
Il grande vantaggio delle promesse è la possibilità di “catenarle” e gestire gli errori in maniera più intuitiva. Esempio:
function fetchData(url) {
return new Promise((resolve, reject) => {
// Simuliamo un’operazione asincrona (es. chiamata fetch)
setTimeout(() => {
const success = true; // ipotizziamo che vada tutto bene
if (success) {
resolve("Dati ottenuti correttamente!");
} else {
reject("Errore durante il recupero dei dati!");
}
}, 1000);
});
}
fetchData("http://esempio.com/api")
.then((dati) => {
console.log(dati); // "Dati ottenuti correttamente!"
return "Nuovi dati elaborati...";
})
.then((valoreSuccessivo) => {
console.log(valoreSuccessivo);
})
.catch((errore) => {
console.error("Si è verificato un errore:", errore);
});
- then() viene invocato in caso di successo. Può ritornare un nuovo valore (o un’altra Promessa) per continuare la catena.
- catch() cattura eventuali errori in una delle promesse precedenti.
Async/Await: la sintassi più pulita per il codice asincrono
A partire da ECMAScript 2017, JavaScript ha introdotto le keyword async e await, che permettono di scrivere codice asincrono in uno stile simile al sincrono, migliorandone la leggibilità.
Come funziona async
?
- Quando dichiari una funzione con la keyword
async
, quella funzione restituisce sempre una Promessa. - All’interno di una funzione
async
puoi usare la keywordawait
per “aspettare” il completamento di un’operazione asincrona (una Promessa).
Come funziona await
?
await
sospende l’esecuzione della funzioneasync
fino a quando la Promessa passata viene risolta o rigettata.- Se la Promessa viene risolta,
await
restituisce il valore di successo. - Se viene rigettata, viene generata un’eccezione che può essere gestita con un blocco
try/catch
.
Ecco lo stesso esempio precedente, ma riscritto usando async/await
:
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("Dati ottenuti correttamente!");
} else {
reject("Errore durante il recupero dei dati!");
}
}, 1000);
});
}
async function getData() {
try {
const dati = await fetchData("http://esempio.com/api");
console.log(dati); // "Dati ottenuti correttamente!"
// Possiamo usare i dati o richiamare un'altra funzione asincrona
const risultatoSuccessivo = await fetchData("http://esempio.com/altri-dati");
console.log(risultatoSuccessivo);
} catch (errore) {
console.error("Si è verificato un errore:", errore);
}
}
// Invocazione
getData();
Notate come, grazie a await
, il codice risulta più lineare e simile allo stile sincrono. Anche la gestione degli errori è semplificata dal blocco try/catch
.
Vantaggi di Async/Await
- Leggibilità del Codice: la scrittura di codice asincrono risulta più intuitiva e meno annidata.
- Debug e Manutenzione: la gestione degli errori con
try/catch
semplifica il flusso e rende il debugging più immediato. - Flessibilità: si può facilmente mischiare
then()
ecatch()
con le funzioniasync
, anche se si consiglia di rimanere coerenti con uno stile.
Best Practice
- Gestione degli Errori: racchiudi sempre le operazioni asincrone in blocchi
try/catch
o utilizza.catch()
. - Parallelizzazione con
Promise.all()
: se hai più operazioni indipendenti, puoi eseguirle in parallelo:jsCopiaModificaasync function getMultipleData() { try { const [risposta1, risposta2] = await Promise.all([ fetchData("url1"), fetchData("url2") ]); console.log(risposta1, risposta2); } catch (error) { console.error("Errore in una delle chiamate:", error); } }
- Handling di Errori Specifici: in alcuni scenari complessi, potresti voler gestire errori diversi in base alla chiamata che li genera. In tal caso, potresti eseguire più blocchi
try/catch
o gestire i singoli.catch()
di ogni promessa. - Evitare di Bloccare il Thread: non usare
await
in luoghi dove eseguire operazioni in parallelo sarebbe più efficiente. Valuta sempre se puoi usarePromise.all()
oPromise.race()
. - Non trascurare la Compatibilità: se devi supportare browser più vecchi, valuta un transpiler (come Babel) o un polyfill per
async/await
.
Conclusioni
Le Promesse e, successivamente, la sintassi Async/Await hanno rivoluzionato il modo di scrivere codice asincrono in JavaScript. Grazie a queste tecniche, il codice diventa più lineare, leggibile e semplice da mantenere, riducendo gli annidamenti tipici delle callback. Saper padroneggiare queste caratteristiche è fondamentale per scrivere applicazioni JavaScript moderne, performanti e manutenibili.
Punti chiave da ricordare:
- Le callback sono alla base dell’asincronia in JS, ma possono portare a codice complesso (callback hell).
- Le Promesse risolvono in parte la questione, introducendo un flusso più chiaro.
async/await
semplifica ulteriormente la scrittura, rendendola più vicina allo stile sincrono.- Gestire correttamente gli errori e ottimizzare le chiamate in parallelo sono elementi essenziali per un buon uso di queste tecniche.
Sperimentare e creare piccoli prototipi è il metodo migliore per familiarizzare con la programmazione asincrona. Buon coding!
Hai trovato utile questo Articolo? Continua a seguire Cyberalchimista sui social (@cyberalchimista) per altri tutorial e consigli sullo sviluppo web! Se hai domande o vuoi condividere i tuoi progetti contattaci.