modulo 07 / lezione 35

Debugging asincrono e interfacce dinamiche

Event loop, microtask, macrotask

Capire perché codice scritto in una sequenza può produrre effetti in un ordine diverso durante l'esecuzione.

55 min Debugging

In parole semplici

JavaScript è single-threaded: una sola cosa alla volta. Ma c’è un meccanismo che fa sembrare il contrario: l’event loop. Conoscerlo è prerequisito per ogni debug asincrono. Senza, ogni bug di timing sembra magia nera.

Parole nuove

  • call stack: pila delle chiamate in corso (modulo 2.2).
  • task queue (macrotask queue): coda di task pronti per eseguire (es. setTimeout, eventi DOM).
  • microtask queue: coda con priorità maggiore (Promise, queueMicrotask).
  • rendering frame: fase di disegno della pagina, ~16ms.

Cosa impari

  • Disegnare l’ordine in cui le cose vengono eseguite.
  • Distinguere task, microtask, render.
  • Spiegare perché un setTimeout(0) non parte “immediatamente”.

Il modello mentale

Immagina un cuoco unico:

  1. Prende un piatto dalla call stack e lo prepara.
  2. Quando il piatto è pronto, controlla la microtask queue: piatti urgenti.
  3. Quando le microtask sono finite, valuta se ridisegnare la sala (render).
  4. Poi prende un piatto dalla task queue: piatti normali.
  5. Torna al passo 2.

Il cuoco non si distrae mai dal piatto in corso (single-threaded). Le code si riempiono mentre cucina, ma vengono lette solo tra un piatto e l’altro.

Esempio canonico

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

Output:

1
4
3
2

Perché:

  1. console.log("1") → eseguito subito.
  2. setTimeout(..., 0) → pianifica un macrotask.
  3. Promise.resolve().then(...) → pianifica una microtask.
  4. console.log("4") → eseguito subito.
  5. Call stack vuoto → drena microtask queue: stampa 3.
  6. Microtask queue vuota → render (qui non c’è UI).
  7. Macrotask queue: stampa 2.

Le microtask vengono prima dei macrotask. È una garanzia del runtime, non un caso.

Cosa genera macrotask

  • setTimeout, setInterval.
  • Eventi DOM (click, input, load).
  • MessageChannel.
  • fetch quando arriva la risposta.

Cosa genera microtask

  • .then / .catch / .finally di Promise.
  • await (zucchero sintattico su Promise).
  • queueMicrotask(fn).
  • MutationObserver.

Il ruolo del render

In browser, tra ogni “giro” dell’event loop il rendering può accadere (~60fps = ogni 16ms). Per il debug visuale:

  • Se cambi DOM, ricevi rendering dopo che la call stack si svuota.
  • Se ammucchi cambi DOM dentro una microtask, vengono batchati: l’utente vede solo lo stato finale.

Bug tipico: imposti loading = true, fai chiamata, alla fine loading = false. Se la chiamata è sincrona o microtask, l’utente non vede mai loading = true.

queueMicrotask

API esplicita per aggiungere una microtask:

queueMicrotask(() => console.log("urgente"));

Utile in raffinamenti di librerie. Nel codice applicativo, raramente serve direttamente; lo usi indirettamente via Promise.then e await.

setTimeout(0) non è “subito”

Sembra “esegui immediatamente”. In realtà: “pianifica nella macrotask queue”. Verrà eseguito dopo tutte le microtask correnti e dopo che la call stack si svuota.

Tipica differenza:

Promise.resolve().then(() => console.log("microtask"));
setTimeout(() => console.log("macrotask"), 0);
console.log("sincrono");
// sincrono
// microtask
// macrotask

Anche specificando 0ms, il macrotask attende.

Debug pratico

In DevTools:

  • Performance panel: registra una sessione e vedi nella timeline microtask, macrotask, render.
  • Async stack traces: con await, vedi anche da dove proveniva la promise originale.

Esempio: race tra UI e dato

async function caricaUtenti() {
  setLoading(true);
  const r = await fetch("/api/utenti");
  const data = await r.json();
  setUtenti(data);
  setLoading(false);
}

Sequenza in event loop:

  1. setLoading(true) → reducer dispatch → microtask di re-render pianificata.
  2. await fetch → la funzione si sospende.
  3. Call stack vuoto → microtask: re-render con loading = true → utente vede loader.
  4. Risposta arriva (più tardi, dopo I/O).
  5. await r.json() → altra sospensione.
  6. setUtenti + setLoading(false) → microtask di re-render.
  7. Render: utente vede utenti.

Bug possibile: se rimuovi await o usi .then male, le sequenze cambiano.

Esercizio guidato

Esegui mentalmente, poi verifica:

console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => {
  console.log("c");
  return Promise.resolve();
}).then(() => console.log("d"));
queueMicrotask(() => console.log("e"));
console.log("f");

Predici l’output. Poi esegui. Discrepanze? Le microtask possono incatenarsi: una .then che ritorna una Promise pianifica un’altra microtask.

Errori frequenti del principiante

  • Pensare che setTimeout(0) sia immediato.
  • Confondere ordine di scrittura con ordine di esecuzione.
  • Dimenticare il render tra microtask e macrotask.
  • Ignorare l’async stack trace. Senza, gli stack di async sono incomprensibili.