Gestione Errori JavaScript

Edoardo Midali
Edoardo Midali

La gestione degli errori in JavaScript è essenziale per creare applicazioni robuste e affidabili. Gli errori sono inevitabili nello sviluppo software, ma una gestione appropriata può trasformare un crash dell’applicazione in un’esperienza utente fluida e informativa.

Tipi di Errori in JavaScript

JavaScript distingue diversi tipi di errori, ognuno con caratteristiche specifiche che aiutano a identificare e risolvere i problemi:

Errori di Sintassi: Si verificano quando il codice non rispetta la sintassi JavaScript. Questi vengono rilevati prima dell’esecuzione e impediscono al codice di funzionare.

Errori di Runtime: Avvengono durante l’esecuzione del programma, come tentare di accedere a proprietà di variabili undefined o chiamare funzioni inesistenti.

Errori Logici: Il codice viene eseguito senza errori tecnici, ma produce risultati incorretti. Questi sono i più difficili da individuare perché non generano eccezioni.

// Errore di sintassi
// let x = 5 + ; // SyntaxError

// Errore di runtime
let obj = null;
console.log(obj.nome); // TypeError: Cannot read property 'nome' of null

// Errore logico
function somma(a, b) {
  return a * b; // Dovrebbe essere a + b
}

Il Blocco try…catch

Il costrutto try...catch è lo strumento principale per gestire gli errori in JavaScript. Permette di “catturare” errori che altrimenti fermerebbero l’esecuzione del programma:

try {
  // Codice che potrebbe generare un errore
  let risultato = operazioneRischiosa();
  console.log("Operazione completata:", risultato);
} catch (errore) {
  // Gestione dell'errore
  console.log("Si è verificato un errore:", errore.message);
} finally {
  // Codice che viene sempre eseguito
  console.log("Pulizia e chiusura operazioni");
}

Il blocco finally è opzionale ma molto utile per operazioni di pulizia che devono essere eseguite indipendentemente dall’esito dell’operazione, come chiudere connessioni di database o liberare risorse.

Lanciare Errori Personalizzati

Oltre a catturare errori, puoi lanciare errori personalizzati usando throw. Questo è utile per validare input o segnalare condizioni impreviste:

function dividi(a, b) {
  if (b === 0) {
    throw new Error("Divisione per zero non consentita");
  }
  return a / b;
}

try {
  let risultato = dividi(10, 0);
  console.log(risultato);
} catch (errore) {
  console.log("Errore nella divisione:", errore.message);
}

Puoi lanciare qualsiasi tipo di valore con throw, ma è buona pratica utilizzare oggetti Error per mantenere coerenza e fornire informazioni utili per il debugging.

Tipi di Errori Built-in

JavaScript fornisce diversi tipi di errori built-in per situazioni specifiche:

// TypeError - tipo di dato non corretto
let numero = 42;
numero.push(3); // TypeError: numero.push is not a function

// ReferenceError - variabile non definita
console.log(variabileInesistente); // ReferenceError

// RangeError - valore fuori intervallo
let array = new Array(-1); // RangeError: Invalid array length

// URIError - URI malformato
decodeURIComponent("%"); // URIError: URI malformed

Conoscere questi tipi ti aiuta a gestire errori specifici in modo più appropriato e a fornire messaggi di errore più informativi agli utenti.

Errori Asincroni

La gestione degli errori diventa più complessa con il codice asincrono. Promise e async/await hanno meccanismi specifici per la gestione degli errori:

Con Promise

fetch("/api/dati")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json();
  })
  .then((dati) => console.log(dati))
  .catch((errore) => {
    console.log("Errore nel caricamento:", errore.message);
  });

Con async/await

async function caricaDati() {
  try {
    const response = await fetch("/api/dati");
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    const dati = await response.json();
    return dati;
  } catch (errore) {
    console.log("Errore nel caricamento:", errore.message);
    return null; // Valore di fallback
  }
}

Strategie di Recupero

Una buona gestione degli errori non si limita a registrarli, ma implementa strategie di recupero quando possibile:

Valori di Fallback

function ottieniConfigutazione() {
  try {
    return JSON.parse(localStorage.getItem("config"));
  } catch (errore) {
    console.warn("Configurazione corrotta, uso valori predefiniti");
    return {
      tema: "light",
      lingua: "it",
      notifiche: true,
    };
  }
}

Retry Automatico

async function operazioneConRetry(operazione, maxTentativi = 3) {
  for (let tentativo = 1; tentativo <= maxTentativi; tentativo++) {
    try {
      return await operazione();
    } catch (errore) {
      if (tentativo === maxTentativi) {
        throw errore; // Ultimo tentativo fallito
      }
      console.log(`Tentativo ${tentativo} fallito, riprovo...`);
      await new Promise((resolve) => setTimeout(resolve, 1000 * tentativo));
    }
  }
}

Logging e Monitoraggio

Un sistema di logging efficace è cruciale per identificare e risolvere problemi in produzione:

class ErrorLogger {
  static log(errore, contesto = {}) {
    const logEntry = {
      message: errore.message,
      stack: errore.stack,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href,
      contesto,
    };

    // In sviluppo: log in console
    if (process.env.NODE_ENV === "development") {
      console.error("Errore registrato:", logEntry);
    }

    // In produzione: invia a servizio di monitoraggio
    if (process.env.NODE_ENV === "production") {
      this.inviaAServizioMonitoraggio(logEntry);
    }
  }

  static inviaAServizioMonitoraggio(logEntry) {
    // Implementazione per servizi come Sentry, LogRocket, etc.
  }
}

Gestione Errori Globali

Per catturare errori non gestiti a livello dell’applicazione:

// Per errori sincroni non catturati
window.addEventListener("error", (event) => {
  console.error("Errore globale:", event.error);
  ErrorLogger.log(event.error, { tipo: "errore_globale" });
});

// Per promise rejections non gestite
window.addEventListener("unhandledrejection", (event) => {
  console.error("Promise rejection non gestita:", event.reason);
  ErrorLogger.log(new Error(event.reason), { tipo: "promise_rejection" });
  event.preventDefault(); // Previene il log automatico in console
});

Best Practices

Fai fallire velocemente: Valida gli input e lancia errori il prima possibile per facilitare il debugging.

Fornisci messaggi di errore significativi: Gli errori dovrebbero spiegare cosa è andato storto e, quando possibile, come risolvere il problema.

Non nascondere gli errori: Anche se gestiti, gli errori dovrebbero essere registrati per analisi future.

Implementa fallback graceful: L’applicazione dovrebbe continuare a funzionare anche quando parti non critiche falliscono.

Testa gli scenari di errore: Scrivi test specifici per verificare che la gestione degli errori funzioni correttamente.

Distingui errori utente da errori sistema: Gli errori causati dall’utente (input non valido) necessitano messaggi diversi da errori tecnici interni.

Una gestione degli errori ben implementata migliora significativamente l’esperienza utente e facilita la manutenzione del codice, trasformando potenziali crash in opportunità per fornire feedback utile e mantenere l’applicazione funzionante.