Introduzione
In JavaScript, molte operazioni non producono un risultato immediato: pensiamo a una richiesta HTTP, alla lettura di un file, a un timer o a un evento utente. In questi casi entra in gioco la programmazione asincrona, e uno degli strumenti più semplici per gestirla è la callback.
Una callback è una funzione passata come argomento a un’altra funzione, con l’obiettivo di essere eseguita in un secondo momento. Questo approccio è utile quando vuoi separare l’avvio di un’operazione dalla logica da eseguire al termine.
In questo tutorial vedremo un sotto-argomento molto pratico: come usare le callback per costruire un piccolo flusso asincrono a più passaggi, ad esempio simulando il caricamento di dati, la loro trasformazione e l’aggiornamento dell’interfaccia. È un caso reale e molto comune nelle applicazioni web.
Codice completo
// Simuliamo una piccola pipeline asincrona:
// 1. carichiamo dei dati
// 2. li trasformiamo
// 3. aggiorniamo l´interfaccia
function loadUserData(userId, callback) {
console.log("Avvio caricamento dati utente...");
setTimeout(() => {
const userData = {
id: userId,
name: "Alice",
role: "developer"
};
callback(null, userData);
}, 1000);
}
function normalizeUserData(userData, callback) {
console.log("Normalizzazione dati...");
setTimeout(() => {
const normalizedData = {
userId: userData.id,
fullName: userData.name.toUpperCase(),
isAdmin: userData.role === "admin"
};
callback(null, normalizedData);
}, 800);
}
function renderUserProfile(profile, callback) {
console.log("Rendering profilo utente...");
setTimeout(() => {
const html = `
<section class="profile">
<h2>${profile.fullName}</h2>
<p>ID utente: ${profile.userId}</p>
<p>Ruolo amministratore: ${profile.isAdmin ? "Sì" : "No"}</p>
</section>
`;
callback(null, html);
}, 500);
}
// Funzione che collega i passaggi in sequenza
function showUserDashboard(userId) {
loadUserData(userId, (error, userData) => {
if (error) {
console.error("Errore nel caricamento:", error);
return;
}
normalizeUserData(userData, (error, normalizedData) => {
if (error) {
console.error("Errore nella normalizzazione:", error);
return;
}
renderUserProfile(normalizedData, (error, html) => {
if (error) {
console.error("Errore nel rendering:", error);
return;
}
console.log("Dashboard pronta:");
console.log(html);
});
});
});
}
showUserDashboard(42); Spiegazione
Il codice mostra una catena di operazioni asincrone costruita con callback. Ogni funzione esegue un task e, al termine, richiama la funzione passata come argomento.
1. Caricamento dei dati
loadUserData simula una richiesta asincrona con setTimeout. Dopo un secondo, restituisce un oggetto utente tramite callback:
- primo parametro: l’errore, che in questo esempio è
null - secondo parametro: il risultato dell’operazione
Questa struttura è molto comune nello stile callback tradizionale: error-first callback.
2. Normalizzazione dei dati
La funzione normalizeUserData prende i dati grezzi e li trasforma in un formato più utile per il resto dell’applicazione. È un passaggio molto realistico: spesso i dati ricevuti da API o database non sono già pronti per essere mostrati.
3. Rendering finale
renderUserProfile converte i dati in markup HTML. Anche qui usiamo una callback per indicare che il rendering è completato e che il risultato è disponibile.
4. Composizione del flusso
La funzione showUserDashboard collega i tre passaggi. Ogni callback annidata dipende dal risultato del passaggio precedente. Questo approccio permette di mantenere il controllo sul flusso asincrono, ma introduce anche una possibile complessità: quando i passaggi aumentano, il codice può diventare difficile da leggere.
Questo è uno dei motivi per cui, in progetti moderni, callback complesse vengono spesso sostituite da Promise o async/await. Tuttavia, capire bene le callback resta fondamentale, perché molte API e librerie storiche le usano ancora.
Best practice
- Usa sempre una convenzione chiara per gli errori: il primo argomento della callback dovrebbe rappresentare l’errore, oppure essere
nullse tutto va bene. - Evita callback troppo annidate: quando il flusso cresce, estrai le funzioni in moduli separati per rendere il codice più leggibile.
- Dai nomi descrittivi alle funzioni:
loadUserData,normalizeUserDataerenderUserProfilecomunicano subito il loro scopo. - Non mischiare responsabilità diverse: una funzione dovrebbe fare una cosa sola. Se carica dati, non dovrebbe anche manipolare il DOM.
- Gestisci sempre il caso di errore: anche se nell’esempio gli errori non vengono generati, nella realtà ogni operazione asincrona può fallire.
- Documenta il formato della callback: specifica quali parametri riceve e in quale ordine, così altri sviluppatori potranno usarla correttamente.
Riepilogo
Le callback sono uno dei concetti più importanti della programmazione asincrona in JavaScript. Permettono di eseguire codice al termine di un’operazione senza bloccare il thread principale. Nel nostro esempio abbiamo visto come usarle per costruire una piccola pipeline: caricamento, trasformazione e rendering.
Il vantaggio principale è la semplicità del modello: una funzione chiama un’altra funzione quando ha finito. Il limite principale è la leggibilità, soprattutto quando si creano molti livelli di annidamento. Per questo motivo è importante scrivere callback pulite, con responsabilità ben definite e con una gestione degli errori coerente.
Se padroneggi bene questo pattern, sarà molto più facile comprendere anche le evoluzioni moderne di JavaScript, come le Promise e async/await.
Approfondisci con risorse ufficiali
- MDN Web Docs - Funzioni callback: documentazione chiara e affidabile sul ruolo delle callback in JavaScript.
- MDN Web Docs - setTimeout: utile per capire come simulare operazioni asincrone e ritardi temporali.
- MDN Web Docs - Asynchronous JavaScript: panoramica generale sulla programmazione asincrona nel browser.
- ECMAScript Language Specification: specifica ufficiale del linguaggio per chi vuole approfondire il comportamento di base di JavaScript.
