Funzioni Asincrone

Le funzioni asincrone in TypeScript, dichiarate con la keyword async, rappresentano il meccanismo moderno per gestire operazioni che richiedono tempo senza bloccare l’esecuzione del programma. Combinate con await, permettono di scrivere codice asincrono che appare sincrono, migliorando leggibilità e manutenibilità rispetto a callback o Promise chain.
Fondamenti e Sintassi
Una funzione async restituisce sempre una Promise, anche se il valore di ritorno non è esplicitamente una Promise. La keyword await può essere usata solo dentro funzioni async per attendere la risoluzione di una Promise.
// Dichiarazione funzione async
async function fetchDati(): Promise<string> {
return "dati"; // Automaticamente avvolto in Promise.resolve()
}
// Arrow function async
const caricaUtente = async (id: string): Promise<User> => {
const response = await fetch(`/api/utenti/${id}`);
return await response.json();
};
// Uso con await
async function main() {
const dati = await fetchDati();
console.log(dati); // "dati"
}
Tipizzazione del Valore di Ritorno
Il tipo di ritorno di una funzione async è sempre Promise<T> dove T è il tipo effettivo restituito. TypeScript inferisce automaticamente questo wrapping.
// Ritorno esplicito Promise<number>
async function calcola(): Promise<number> {
return 42; // Automaticamente Promise.resolve(42)
}
// Inferenza automatica
async function elabora() {
return { risultato: "ok" }; // Promise<{ risultato: string }>
}
// Con await, si ottiene il tipo unwrapped
async function usa() {
const numero = await calcola(); // numero è number, non Promise<number>
const obj = await elabora(); // obj è { risultato: string }
}
Await e Gestione Promise
Await pausa l’esecuzione della funzione async fino alla risoluzione della Promise, restituendo il valore risolto o lanciando un errore se la Promise viene rigettata.
async function esempiAwait() {
// Await su Promise
const utente = await fetchUtente("123");
// Await su valore non-Promise (restituisce immediatamente)
const immediato = await 42; // 42
// Multiple await sequenziali
const dati1 = await fetch("/api/1").then((r) => r.json());
const dati2 = await fetch("/api/2").then((r) => r.json());
// Await su funzione async
const risultato = await altroAsync();
}
Gestione Errori con Try-Catch
Gli errori in operazioni asincrone si gestiscono con try-catch standard, catturando rigetti di Promise come eccezioni sincrone.
async function caricaDatiSicuro(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/utenti/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const utente = await response.json();
return utente;
} catch (error) {
console.error("Errore caricamento:", error);
return null;
}
}
// Finally per cleanup
async function conCleanup() {
const connessione = await apriConnessione();
try {
await eseguiOperazione(connessione);
} catch (error) {
console.error(error);
} finally {
await connessione.close();
}
}
Esecuzione Parallela
Mentre await sequenziali eseguono operazioni una dopo l’altra, Promise.all permette esecuzione parallela di multiple Promise.
// Sequenziale: lento
async function sequenziale() {
const utente1 = await fetchUtente("1"); // 1 secondo
const utente2 = await fetchUtente("2"); // 1 secondo
const utente3 = await fetchUtente("3"); // 1 secondo
// Totale: 3 secondi
return [utente1, utente2, utente3];
}
// Parallelo: veloce
async function parallelo() {
const [utente1, utente2, utente3] = await Promise.all([
fetchUtente("1"),
fetchUtente("2"),
fetchUtente("3"),
]);
// Totale: 1 secondo (tutte in parallelo)
return [utente1, utente2, utente3];
}
// Con map
async function caricaMultipli(ids: string[]) {
const promesse = ids.map((id) => fetchUtente(id));
const utenti = await Promise.all(promesse);
return utenti;
}
Promise.allSettled e Promise.race
Varianti di Promise.all per scenari diversi: allSettled attende tutte le Promise indipendentemente da successo/fallimento, race restituisce la prima completata.
// AllSettled: tutte le Promise, anche fallite
async function caricaTutti(ids: string[]) {
const promesse = ids.map((id) => fetchUtente(id));
const risultati = await Promise.allSettled(promesse);
risultati.forEach((risultato, i) => {
if (risultato.status === "fulfilled") {
console.log(`Utente ${ids[i]}:`, risultato.value);
} else {
console.error(`Errore ${ids[i]}:`, risultato.reason);
}
});
}
// Race: prima Promise completata
async function conTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeoutMs)
);
return await Promise.race([promise, timeout]);
}
Async IIFE
Immediately Invoked Function Expression asincrone permettono uso di await a livello top-level in contesti che non lo supportano nativamente.
// IIFE async per top-level await
(async () => {
const dati = await fetchDati();
console.log(dati);
})();
// Con gestione errori
(async () => {
try {
const risultato = await operazioneComplessa();
processaRisultato(risultato);
} catch (error) {
console.error("Errore:", error);
}
})();
// Top-level await (ES2022, supportato in moduli)
// In file .ts con type: module
const configurazione = await caricaConfig();
inizializzaApp(configurazione);
Async con Array Methods
Combinare funzioni async con metodi di array richiede attenzione: map crea array di Promise che devono essere awaitate con Promise.all.
const ids = ["1", "2", "3"];
// ERRATO: ritorna array di Promise
const utenti = ids.map(async (id) => await fetchUtente(id));
// utenti è Promise<User>[], non User[]
// CORRETTO: await Promise.all
const utentiCorretti = await Promise.all(
ids.map(async (id) => await fetchUtente(id))
);
// forEach: non aspetta le Promise
ids.forEach(async (id) => {
await fetchUtente(id); // Esegue tutte insieme, non sequenzialmente
});
// For-of per esecuzione sequenziale
for (const id of ids) {
const utente = await fetchUtente(id);
processaUtente(utente);
}
Async Iterator e For-Await-Of
Iteratori asincroni permettono elaborazione di stream di dati asincroni con for-await-of.
// Generatore asincrono
async function* generaDati() {
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
yield i;
}
}
// Consumo con for-await-of
async function elabora() {
for await (const valore of generaDati()) {
console.log(valore);
}
}
// Iteratore asincrono personalizzato
class DataStream {
async *[Symbol.asyncIterator]() {
let pagina = 1;
while (true) {
const dati = await fetchPagina(pagina++);
if (dati.length === 0) break;
yield* dati;
}
}
}
const stream = new DataStream();
for await (const item of stream) {
processaItem(item);
}
Composizione di Funzioni Async
Funzioni async possono chiamarsi a vicenda, componendo operazioni asincrone complesse da building blocks più semplici.
async function fetchUtente(id: string): Promise<User> {
const response = await fetch(`/api/utenti/${id}`);
return await response.json();
}
async function fetchPostUtente(userId: string): Promise<Post[]> {
const response = await fetch(`/api/utenti/${userId}/posts`);
return await response.json();
}
// Composizione
async function caricaUtenteCompleto(id: string) {
const utente = await fetchUtente(id);
const posts = await fetchPostUtente(id);
return {
...utente,
posts,
};
}
// Composizione con dipendenze
async function workflow(userId: string) {
const utente = await fetchUtente(userId);
const impostazioni = await fetchImpostazioni(utente.id);
const dati = await fetchDatiPersonalizzati(utente, impostazioni);
return elabora(dati);
}
Retry Pattern
Implementare retry automatici per operazioni asincrone che possono fallire temporaneamente.
async function conRetry<T>(
operazione: () => Promise<T>,
maxTentativi: number = 3,
delayMs: number = 1000
): Promise<T> {
for (let tentativo = 1; tentativo <= maxTentativi; tentativo++) {
try {
return await operazione();
} catch (error) {
if (tentativo === maxTentativi) {
throw error;
}
console.log(`Tentativo ${tentativo} fallito, riprovo...`);
await new Promise((resolve) => setTimeout(resolve, delayMs * tentativo));
}
}
throw new Error("Impossibile completare operazione");
}
// Uso
async function fetchConRetry(url: string) {
return await conRetry(() => fetch(url).then((r) => r.json()), 3, 500);
}
Cancellazione e AbortController
Gestire cancellazione di operazioni asincrone usando AbortController per fetch e altre API che lo supportano.
async function fetchCancellabile(
url: string,
signal: AbortSignal
): Promise<any> {
const response = await fetch(url, { signal });
return await response.json();
}
async function esempio() {
const controller = new AbortController();
// Cancella dopo 5 secondi
setTimeout(() => controller.abort(), 5000);
try {
const dati = await fetchCancellabile("/api/dati", controller.signal);
console.log(dati);
} catch (error) {
if (error.name === "AbortError") {
console.log("Operazione cancellata");
}
}
}
Async con Classi
Metodi di classe possono essere async, permettendo operazioni asincrone incapsulate in oggetti.
class UserService {
constructor(private baseUrl: string) {}
async getUtente(id: string): Promise<User> {
const response = await fetch(`${this.baseUrl}/utenti/${id}`);
return await response.json();
}
async creaUtente(dati: Partial<User>): Promise<User> {
const response = await fetch(`${this.baseUrl}/utenti`, {
method: "POST",
body: JSON.stringify(dati),
headers: { "Content-Type": "application/json" },
});
return await response.json();
}
async aggiornaUtente(id: string, dati: Partial<User>): Promise<User> {
const response = await fetch(`${this.baseUrl}/utenti/${id}`, {
method: "PUT",
body: JSON.stringify(dati),
headers: { "Content-Type": "application/json" },
});
return await response.json();
}
}
const service = new UserService("https://api.example.com");
const utente = await service.getUtente("123");
Performance e Best Practices
Evitare await non necessari in sequenza quando le operazioni sono indipendenti. Usare Promise.all per parallelizzazione quando possibile.
// LENTO: await inutili in sequenza
async function lento() {
const a = await operazioneA();
const b = await operazioneB(); // Non dipende da a
const c = await operazioneC(); // Non dipende da a o b
return [a, b, c];
}
// VELOCE: parallelizzazione
async function veloce() {
const [a, b, c] = await Promise.all([
operazioneA(),
operazioneB(),
operazioneC(),
]);
return [a, b, c];
}
// Con dipendenze
async function conDipendenze() {
const a = await operazioneA();
const [b, c] = await Promise.all([
operazioneB(a), // Dipende da a
operazioneC(a), // Dipende da a
]);
return [a, b, c];
}
Testing di Funzioni Async
Le funzioni async si testano attendendo la loro Promise o usando async/await nei test.
// Test con async/await
test("carica utente", async () => {
const utente = await fetchUtente("123");
expect(utente.id).toBe("123");
});
// Test con .then
test("carica utente con then", () => {
return fetchUtente("123").then((utente) => {
expect(utente.id).toBe("123");
});
});
// Mock di funzioni async
const mockFetch = jest.fn().mockResolvedValue({
json: async () => ({ id: "123", nome: "Test" }),
});
Conclusioni
Le funzioni asincrone in TypeScript forniscono sintassi elegante e leggibile per gestire operazioni asincrone, trasformando codice basato su Promise chain in strutture che appaiono sincrone. Await semplifica gestione errori con try-catch standard, mentre combinazioni con Promise.all permettono parallelizzazione efficiente. Comprendere quando usare esecuzione sequenziale versus parallela, gestire errori appropriatamente, e sfruttare pattern come retry e cancellazione risulta essenziale per applicazioni robuste che gestiscono I/O, network, e altre operazioni non-bloccanti.