Se scrivi JavaScript da più di qualche mese, hai sentito il dolore delle callback profondamente annidate e delle catene .then() aggrovigliate. async/await, introdotto in ES2017, ha risolto tutto questo — eppure gli sviluppatori continuano a incappare negli stessi tre o quattro tranelli. Analizziamo tutto correttamente: come funziona, come gestire bene gli errori e i pattern di esecuzione parallela che contano davvero per le prestazioni.

Le basi — funzioni async e await

Una funzione async restituisce sempre una Promise. Al suo interno, await mette in pausa l'esecuzione finché la Promise attesa non si risolve. Questo è l'intero modello mentale:

js
async function fetchUserProfile(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const profile = await response.json();
  return profile; // wrapped in a Promise automatically
}

// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));

// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);

La parola chiave await può essere usata solo all'interno di una funzione async — o al livello superiore di un modulo ES (ne parleremo più avanti). Usarla altrove è un errore di sintassi.

Gestione degli errori — Il modo giusto e quello sbagliato

È qui che la maggior parte dei tutorial va fuori strada. L'istinto è di avvolgere tutto in try/catch e considerarlo fatto. Funziona, ma i blocchi catch vuoti sono un odore di codice che maschera bug reali:

js
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
  try {
    const res = await fetch('/api/config');
    return await res.json();
  } catch (err) {
    // swallowed — you'll never know what broke
  }
}

// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
  try {
    const res = await fetch('/api/config');
    if (!res.ok) {
      throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
    }
    return await res.json();
  } catch (err) {
    console.error('loadConfig error:', err.message);
    return null; // caller can check for null
  }
}

Preferisco un pattern in cui le funzioni async restituiscono null in caso di fallimento o lanciano errori intenzionali. Quello che evito è catturare un errore, registrarlo e poi restituire un valore che fa credere al chiamante che la richiesta sia andata a buon fine.

Consiglio pro: Se hai più await in un blocco try, un singolo catch li gestisce tutti — ma perdi il contesto su quale ha fallito. Per flussi complessi, considera blocchi try/catch separati o un helper come await to(promise) che restituisce tuple [error, data].

Sequenziale vs Parallelo — La trappola delle prestazioni

Questo è l'errore che vedo più spesso nel codice in produzione. Quando usi await per ogni chiamata una dopo l'altra, le esegui in sequenza — anche quando sono completamente indipendenti l'una dall'altra:

js
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
  const user     = await fetchUser(userId);       // 300ms
  const orders   = await fetchOrders(userId);     // 300ms
  const settings = await fetchSettings(userId);  // 300ms
  return { user, orders, settings };
}

// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
  const [user, orders, settings] = await Promise.all([
    fetchUser(userId),
    fetchOrders(userId),
    fetchSettings(userId)
  ]);
  return { user, orders, settings };
}

Promise.all() avvia tutte e tre le richieste nello stesso momento e aspetta che tutte si completino. Se una di esse viene rifiutata, l'intera cosa viene rifiutata. Di solito è quello che vuoi per il caricamento in stile dashboard dove tutti i dati sono necessari.

Promise.allSettled — Quando il fallimento parziale va bene

A volte vuoi avviare più richieste e usare ciò che ritorna con successo, anche se alcune falliscono. Promise.allSettled() è costruito esattamente per questo:

js
async function loadWidgets(widgetIds) {
  const results = await Promise.allSettled(
    widgetIds.map(id => fetchWidget(id))
  );

  const widgets = [];
  const errors  = [];

  for (const result of results) {
    if (result.status === 'fulfilled') {
      widgets.push(result.value);
    } else {
      errors.push(result.reason.message);
    }
  }

  if (errors.length > 0) {
    console.warn('Some widgets failed to load:', errors);
  }

  return widgets; // return whatever succeeded
}

Questo pattern è ottimo per gli elementi UI non critici — come una barra laterale con più sezioni indipendenti. Se una fallisce, mostri le altre invece di lasciare la pagina vuota.

async nei cicli — Il problema con forEach

Questa ha bruciato tutti almeno una volta. Array.forEach() non attende le callback async — le avvia e va avanti immediatamente. Il ciclo termina prima che qualsiasi lavoro async sia completato:

js
const orderIds = [101, 102, 103, 104];

// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
  await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed

// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
  await processOrder(id);
}
console.log('done'); // prints after all orders are processed

// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processed

Usa for...of quando l'ordine è importante o quando devi limitare le richieste (elabora una alla volta). Usa Promise.all(map(...)) quando vuoi il massimo parallelismo e non hai bisogno di garanzie sequenziali.

await di primo livello nei moduli ES

Da ES2022, puoi usare await al livello superiore di un modulo ES — nessuna funzione wrapper necessaria. Questo è molto importante per l'inizializzazione dei moduli che dipende da dati async:

js
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config   = await response.json();

export const API_BASE_URL  = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;
js
// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';

console.log(API_BASE_URL); // guaranteed to be loaded

await di primo livello funziona in Node.js 14.8+ con "type": "module" in package.json, e in tutti i browser moderni tramite moduli ES nativi. L'esecuzione del modulo importante viene sospesa finché il modulo atteso non si risolve completamente — che è esattamente la garanzia di cui hai bisogno.

Una pipeline reale — Fetch, analisi e trasformazione

Ecco una pipeline async realistica che combina tutto: recupero da un'API, gestione degli errori HTTP, trasformazione dei dati e ripiego in modo elegante in caso di errore:

js
async function getProductCatalog(categoryId) {
  // Step 1: fetch raw data
  const response = await fetch(
    `https://api.shop.example.com/categories/${categoryId}/products`,
    { headers: { Authorization: `Bearer ${getAuthToken()}` } }
  );

  if (!response.ok) {
    throw new Error(`Catalog fetch failed: ${response.status}`);
  }

  // Step 2: parse JSON
  const raw = await response.json();

  // Step 3: transform into the shape your UI needs
  const products = raw.items.map(item => ({
    id:       item.product_id,
    name:     item.display_name,
    price:    (item.price_cents / 100).toFixed(2),
    inStock:  item.inventory_count > 0,
    imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
  }));

  // Step 4: filter out anything that's been discontinued
  return products.filter(p => !p.discontinued);
}

// Usage
try {
  const catalog = await getProductCatalog('electronics');
  renderProductGrid(catalog);
} catch (err) {
  showErrorBanner(`Could not load products: ${err.message}`);
}

Strumenti utili

Quando stai eseguendo il debug di codice async che elabora payload JSON, questi strumenti aiutano: JSON Formatter per ispezionare le risposte API, JSON Validator per rilevare payload malformati prima che raggiungano il tuo codice, e JS Formatter per pulire il codice delle funzioni async. Per la specifica completa di async/await, la guida async di MDN è il riferimento più completo, e la specifica TC39 copre la semantica esatta se ne hai bisogno.

In conclusione

async/await rende JavaScript asincrono leggibile — ma le trappole sono reali. Controlla sempre response.ok prima di analizzare, non lasciare mai che i blocchi catch inghiottano gli errori silenziosamente, usa Promise.all() per le chiamate parallele indipendenti, e stai lontano da forEach con le callback async. Consolida queste abitudini e il tuo codice async sarà sia veloce che debuggable.