Debugging asincrono in JavaScript con Promise, async/await e console

by Anastasia P.
SHARE
Debugging asincrono in JavaScript con Promise, async/await e console
© Guida-HTML5.it

Introduzione

Il codice asincrono è una delle principali fonti di difficoltà durante il debugging JavaScript. Una funzione che esegue una richiesta, un timer o un’operazione basata su una Promise non termina necessariamente nello stesso momento in cui viene chiamata. Di conseguenza, un messaggio visualizzato nella console può comparire dopo altri log apparentemente collegati, oppure un errore può rimanere nascosto perché una Promise non viene gestita correttamente.

In questo tutorial vedremo come usare la console del browser per analizzare il flusso asincrono con async/await, try/catch, Promise.allSettled(), console.assert() e oggetti di contesto. L’obiettivo è capire non solo se si verifica un errore, ma anche quando si verifica, quale operazione lo ha generato e quali dati erano disponibili in quel momento.

Apri gli strumenti per sviluppatori del browser con F12 oppure con Ctrl+Shift+I su Windows/Linux e Cmd+Option+I su macOS. Seleziona quindi la scheda Console, dove potrai osservare l’ordine di esecuzione delle operazioni.

Codice completo

const utenti = [1, 2, 3];

function simulaRichiestaUtente(id) {
  return new Promise((resolve, reject) => {
    const durata = Math.floor(Math.random() * 1200) + 300;

    setTimeout(() => {
      // L´utente 2 simula una risposta non valida
      if (id === 2) {
        reject(new Error("Impossibile caricare l´utente " + id));
        return;
      }

      resolve({
        id,
        nome: "Utente " + id,
        stato: "attivo"
      });
    }, durata);
  });
}

async function caricaUtenti() {
  console.log("[START] Caricamento utenti", {
    quantita: utenti.length,
    avviatoAlle: new Date().toISOString()
  });

  const risultati = await Promise.allSettled(
    utenti.map(id => simulaRichiestaUtente(id))
  );

  risultati.forEach((risultato, indice) => {
    const idUtente = utenti[indice];

    if (risultato.status === "fulfilled") {
      console.log("[OK] Utente caricato", {
        id: idUtente,
        dati: risultato.value
      });

      console.assert(
        risultato.value.id === idUtente,
        "L´ID ricevuto non corrisponde a quello richiesto"
      );
    } else {
      console.error("[ERRORE] Caricamento fallito", {
        id: idUtente,
        messaggio: risultato.reason.message,
        errore: risultato.reason
      });
    }
  });

  console.log("[FINE] Elaborazione completata");
}

async function avviaApplicazione() {
  try {
    await caricaUtenti();
  } catch (errore) {
    console.error("[FATALE] Errore non gestito", errore);
  }
}

avviaApplicazione();

Spiegazione

Osservare l’ordine reale delle operazioni

La funzione simulaRichiestaUtente() restituisce una Promise. La risposta viene risolta dopo un intervallo casuale, quindi gli utenti potrebbero essere completati in un ordine diverso da quello dell’array originale. Questo comportamento è tipico delle richieste HTTP: una richiesta avviata per prima non è necessariamente quella che termina per prima.

Il primo log contiene l’etichetta [START]. Le etichette testuali aiutano a distinguere rapidamente le fasi dell’applicazione. Quando il codice viene eseguito in un progetto reale, prefix come [AUTH], [API] o [UI] rendono la console molto più leggibile.

Perché usare Promise.allSettled()

Promise.all() interrompe l’elaborazione appena una delle Promise viene rifiutata. Questo può essere corretto quando tutti i dati sono indispensabili, ma rende più difficile capire quali operazioni siano riuscite e quali siano fallite.

Promise.allSettled(), invece, attende tutte le operazioni e restituisce un array con due possibili stati:

  • fulfilled: la Promise è stata completata correttamente e il risultato si trova in risultato.value;
  • rejected: la Promise è fallita e l’errore si trova in risultato.reason.

Questa distinzione è molto utile durante il debugging perché consente di esaminare il comportamento completo del gruppo di operazioni, anche quando soltanto una richiesta presenta problemi.

Raggruppare le informazioni in un oggetto

Invece di concatenare molti valori in una stringa, il codice passa alla console un oggetto:

console.error("[ERRORE] Caricamento fallito", {
  id: idUtente,
  messaggio: risultato.reason.message,
  errore: risultato.reason
});

Nel browser puoi espandere l’oggetto e analizzare le sue proprietà. Questo approccio è più efficace rispetto a messaggi come "Errore utente 2", perché conserva il contesto: identificativo, messaggio originale e oggetto errore completo.

Verificare le ipotesi con console.assert()

console.assert() mostra un messaggio soltanto quando la condizione passata è falsa. Nel codice viene usato per verificare che l’ID ricevuto dal servizio corrisponda a quello richiesto:

console.assert(
  risultato.value.id === idUtente,
  "L´ID ricevuto non corrisponde a quello richiesto"
);

È uno strumento utile per controllare le condizioni che devono essere sempre vere. Non sostituisce la validazione dell’applicazione e non dovrebbe essere usato come unico meccanismo di gestione degli errori, ma aiuta a individuare rapidamente dati incoerenti durante lo sviluppo.

Gestire gli errori con try/catch

La funzione avviaApplicazione() contiene un blocco try/catch. Se caricaUtenti() genera un errore non gestito, il controllo passa al blocco catch. È importante ricordare che il try/catch intercetta gli errori asincroni soltanto quando si utilizza await all’interno del blocco:

try {
  await caricaUtenti();
} catch (errore) {
  console.error("[FATALE] Errore non gestito", errore);
}

Una Promise rifiutata senza await o senza .catch() può generare un avviso di tipo Unhandled Promise Rejection nella console. Questo tipo di avviso non deve essere ignorato: spesso indica che un percorso di errore non è stato progettato correttamente.

Best practice

  • Usa messaggi brevi ma descrittivi, preferibilmente con un prefisso che identifichi il modulo o la fase dell’applicazione.
  • Registra gli oggetti separatamente dal testo, così il browser può mostrarne le proprietà in forma esplorabile.
  • Inserisci nei log gli identificativi utili, come l’ID della richiesta, dell’utente o del componente coinvolto.
  • Gestisci sempre le Promise con await, .then() e .catch(), oppure con un meccanismo equivalente.
  • Usa Promise.allSettled() quando devi analizzare tutte le operazioni anche in presenza di errori parziali.
  • Utilizza console.assert() per verificare invarianti e ipotesi durante lo sviluppo.
  • Rimuovi o disattiva i log troppo dettagliati in produzione, soprattutto se contengono dati personali o informazioni sensibili.
  • Non affidarti esclusivamente al colore dei messaggi nella console: aggiungi sempre contesto testuale e dati utili.

Riepilogo

Il debugging del codice asincrono richiede una visione chiara dell’ordine temporale delle operazioni. Le Promise possono completarsi in tempi diversi e gli errori possono rimanere nascosti se non vengono gestiti esplicitamente.

Per lavorare in modo efficace, usa log con etichette coerenti, registra gli oggetti di contesto, analizza i risultati con Promise.allSettled() e verifica le condizioni importanti con console.assert(). Infine, proteggi il flusso principale con try/catch e controlla la console alla ricerca di Promise rifiutate non gestite.

Approfondisci con risorse ufficiali

  • MDN Web Docs: documentazione sulle Promise JavaScript e sui relativi stati.
  • MDN Web Docs: guida a async function e await.
  • MDN Web Docs: riferimento per Promise.allSettled().
  • Chrome for Developers: guida alla Console DevTools e alla visualizzazione degli oggetti.
  • JavaScript.info: capitoli dedicati a Promise, async/await e gestione degli errori.

SHARE