Gestire l'Asincronia: Callback, Promise e Async/Await
L'evoluzione del codice asincrono in Node.js: come passare dal disordine delle callback alla pulizia di async/await.
JavaScript è progredito enormemente nel modo in cui gestisce l’attesa. Siamo passati dalle “funzioni dentro le funzioni” a una sintassi che sembra quasi linguaggio naturale.
In Node.js, l’asincronia non è un’opzione, è lo standard. Poiché il sistema deve gestire molte operazioni contemporaneamente senza bloccare l’Event Loop, negli anni sono nati diversi strumenti per scrivere questo tipo di codice.
1. Le Callback: Il metodo “vecchia scuola”
Una Callback è una funzione passata come argomento a un’altra funzione. In JavaScript questo è possibile perché le funzioni sono cittadini di prima classe (First-Class Functions): possono essere assegnate a variabili, passate come parametri e restituite da altre funzioni.
setTimeout(() => {
console.log("Eseguito dopo 2 secondi")
}, 2000)
Il problema: Callback Hell
Quando devi concatenare molte operazioni, il codice inizia a “scivolare” verso destra, creando una piramide illeggibile chiamata Callback Hell.
// La piramide della rovina
operazione1(() => {
operazione2(() => {
operazione3(() => {
// Difficile da leggere e da debuggare...
})
})
})
2. Le Promise: Una boccata d’aria
Una Promise rappresenta un valore che non conosciamo ancora, ma che “promettiamo” di restituire in futuro. Una promessa può trovarsi in tre stati:
Stato iniziale: l’operazione è ancora in corso.
L’operazione è terminata con successo e ha restituito un valore.
L’operazione è fallita e ha restituito un errore.
Ecco come creiamo una promessa che controlla il risultato di una moltiplicazione:
function ottieniProdotto(a, b) {
return new Promise((resolve, reject) => {
const prodotto = a * b
if (prodotto <= 50) {
resolve("Risultato accettabile")
} else {
reject(new Error("Il risultato è troppo alto!"))
}
})
}
3. Async / Await: Lo standard moderno
Questa è l’evoluzione finale. async/await è costruito sopra le Promise, ma rende il codice lineare, come se fosse sincrono.
- async: Trasforma una funzione normale in una che restituisce sempre una Promise.
- await: Dice a JavaScript di “fermare” l’esecuzione della funzione corrente finché la promessa non viene risolta.
async function eseguiCalcolo() {
try {
const risultato = await ottieniProdotto(5, 5)
console.log(risultato) // Risultato accettabile
} catch (error) {
console.error("Errore:", error.message)
}
}
È pulito, gestisce gli errori con il classico try/catch e rende
lo stack trace degli errori molto più comprensibile durante il debug.
4. Gestire più Operazioni in Parallelo
Finora abbiamo gestito operazioni una alla volta. Ma cosa succede quando hai bisogno di attendere più risultati contemporaneamente? async/await usato naivamente porta a un errore classico: aspettare in fila operazioni che potrebbero girare in parallelo.
// ❌ Lento: le tre operazioni girano in sequenza
async function lento() {
const utente = await fetchUtente(1) // aspetta...
const ordini = await fetchOrdini(1) // poi aspetta...
const prodotti = await fetchProdotti() // poi aspetta ancora...
}
Promise.all() risolve questo: avvia tutte le operazioni contemporaneamente e aspetta che tutte siano completate. Se una fallisce, l’intera operazione viene rigettata.
// ✅ Veloce: le tre operazioni girano in parallelo
async function veloce() {
const [utente, ordini, prodotti] = await Promise.all([
fetchUtente(1),
fetchOrdini(1),
fetchProdotti(),
])
}
Se le operazioni sono indipendenti tra loro — non usi il risultato dell’una
per avviare l’altra — usa sempre Promise.all(). Può ridurre i
tempi di risposta drasticamente.
Esiste anche Promise.race(), che restituisce il risultato della prima Promise che si risolve, ignorando le altre. Il caso d’uso classico è implementare un timeout:
const risultato = await Promise.race([
fetchDati("https://api.esempio.it/dati"),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout!")), 5000)
),
])
Il Promise Chaining: riconoscerlo nel codice legacy
Prima di async/await, le Promise venivano concatenate con .then() e .catch(). Non lo scriverai spesso, ma lo incontrerai in codebase esistenti ed è importante saperlo leggere.
fetchUtente(1)
.then((utente) => fetchOrdini(utente.id))
.then((ordini) => console.log(ordini))
.catch((errore) => console.error(errore))
La versione equivalente in async/await è quella che hai già visto: più lineare, più facile da debuggare. Il chaining e async/await sono due sintassi diverse per la stessa cosa — le Promise — ed è per questo che si usano insieme senza problemi.
Cosa scegliere oggi?
Da usare solo se strettamente necessario (vecchie API o librerie datate).
Scegli questo nel 99% dei casi. È lo standard industriale attuale per scrivere codice leggibile e robusto.
Da affiancare ad async/await ogni volta che hai operazioni indipendenti da parallelizzare o un timeout da implementare.
Hai padroneggiato l’asincronia, ora manca solo un ingrediente per costruire
progetti veri: la gestione delle dipendenze. Nel prossimo articolo scoprirai
NPM e il file package.json.