La legge di Murphy afferma che tutto ciò che può andare male alla fine andrà male. Questo si applica molto bene al mondo della programmazione. Se create un’applicazione, è probabile che si generino bug e altri problemi. Gli errori in JavaScript sono uno di questi problemi!

Il successo di un prodotto software dipende da come le persone che lo hanno progettato riescono a risolvere questi problemi prima che generino un danno per i loro utenti. E JavaScript, tra tutti i linguaggi di programmazione, è noto per la sua scarsa gestione degli errori.

Se state costruendo un’applicazione JavaScript, c’è un’alta probabilità che, a un certo punto, riscontriate problemi con i tipi di dati. Se non è così, allora potreste finire per sostituire un undefinedcon un null o un operatore triplo uguale (===) con un operatore doppio uguale (==).

Commettere errori è umano. Ecco perché in questo articolo vi mostreremo tutto quello che c’è da sapere sulla gestione degli errori in JavaScript.

L’articolo vi guiderà alla scoperta degli errori di base in JavaScript e vi spiegherà i vari errori che potreste incontrare. Imparerete poi a identificare e correggere questi errori. Ci sono anche un paio di consigli per gestire efficacemente gli errori in ambienti di produzione.

Ma non indugiamo oltre, cominciamo!

Guarda la Nostra Video-Guida alla Gestione degli Errori JavaScript

Cosa Sono gli Errori JavaScript?

Gli errori nella programmazione si riferiscono a sivostrazioni che non permettono a un programma di funzionare normalmente. Può accadere quando un programma non sa come gestire il lavoro in corso, come quando si cerca di aprire un file inesistente o di raggiungere un endpoint API basato sul web mentre non c’è connettività di rete.

Queste sivostrazioni spingono il programma a lanciare errori all’utente, affermando che non sa come procedere. Il programma raccoglie quante più informazioni possibili sull’errore e poi segnala che non può andare avanti.

Chi lavora nella programmazione e ha una certa lungimiranza, cerca di prevedere e coprire questi scenari in modo che l’utente non debba interpretare autonomamente un messaggio di errore tecnico come “404”. Basta cambiare il messaggio usando parole più comprensibili come “La pagina non è stata trovata”.

Gli errori in JavaScript sono oggetti che compaiono ogni volta che si verifica un errore di programmazione. Questi oggetti contengono ampie informazioni sul tipo di errore, l’istruzione che ha causato l’errore e lo stack trace di quando si è verificato l’errore. JavaScript permette inoltre a chi programma di creare errori personalizzati per fornire informazioni extra durante il debug dei problemi.

Proprietà di un Errore

Ora che la definizione di un errore JavaScript è chiara, è il momento di entrare nei dettagli.

Gli errori in JavaScript contengono alcune proprietà standard e personalizzate che aiutano a capire la causa e gli effetti dell’errore. Per impostazione predefinita, gli errori in JavaScript contengono tre proprietà:

  1. message: il valore stringa che riporta il messaggio di errore;
  2. name: il tipo di errore che si è verificato (lo approfondiremo nella prossima sezione);
  3. stack: lo stack trace del codice eseguito quando si è verificato l’errore.

Inoltre, gli errori possono anche portare proprietà come columnNumber, lineNumber, fileName, ecc. per descrivere meglio l’errore. Tuttavia, queste proprietà non sono standard e possono essere presenti o meno in ogni oggetto di errore generato dalla vostra applicazione JavaScript.

Capire lo Stack Trace

Lo stack trace (che potremmo tradurre letteralmente come “traccia dello stack”) è l’elenco delle chiamate di metodo in cui si trovava un programma quando si è verificato un evento come un’eccezione o un avvertimento. Ecco come appare uno stack trace accompagnato da un’eccezione:

L’errore “TypeError: Numeric argument is expected” è mostrato su uno sfondo grigio con ulteriori dettagli sullo stack
Esempio di Stack Trace.

Come potete vedere, inizia pubblicando il nome e il messaggio dell’errore, seguito da una lista di metodi che sono stati chiamati. Ogni chiamata di metodo indica la posizione del suo codice sorgente e la linea in cui è stato richiamato. Potete usare questi dati per navigare nel vostro codebase e identificare il pezzo di codice che sta causando l’errore.

Questo elenco di metodi è organizzato in modo impilato (stacked). Mostra in che punto la vostra eccezione è stata lanciata per la prima volta e come si è propagata attraverso le chiamate impilate. L’implementazione di un catch per l’eccezione non permetterà che si propaghi attraverso lo stack e mandi in crash il vostro programma. Tuttavia, in alcune situazioni potreste intenzionalmente voler lasciare irrisolti gli errori fatali per mandare in crash il programma.

Errori vs Eccezioni

La maggior parte delle persone di solito considera gli errori e le eccezioni come la stessa cosa. Tuttavia, è essenziale ricordare la leggera ma fondamentale differenza tra loro.

Per capirlo meglio, facciamo un rapido esempio. Ecco come potete definire un errore in JavaScript:

const wrongTypeError = TypeError("Wrong type found, expected character")

E questo è come l’oggetto wrongTypeError diventa un’eccezione:

throw wrongTypeError

Tuttavia, la maggior parte delle persone tende a usare la forma abbreviata che definisce gli oggetti di errore mentre li lancia:

throw TypeError("Wrong type found, expected character")

Questa è una pratica standard. Tuttavia, è una delle ragioni per cui gli sviluppatori tendono a confondere eccezioni ed errori. Pertanto, conoscere i fondamenti è vitale anche se usate le abbreviazioni per velocizzare il lavoro.

Tipi di Errori in JavaScript

Esiste una serie di tipi di errore predefiniti in JavaScript. Sono scelti e definiti automaticamente dal runtime JavaScript ogni volta che il programmatore o la programmatrice non gestisce esplicitamente gli errori nell’applicazione.

Questa sezione vi guiderà attraverso alcuni dei più comuni tipi di errore in JavaScript per capire quando e perché si verificano.

RangeError

Un RangeError viene lanciato quando una variabile viene impostata con un valore al di fuori del suo intervallo di valori legali. Di solito si verifica quando si passa un valore come argomento a una funzione e il valore dato non si trova nell’intervallo dei parametri della funzione. A volte può essere difficile da risolvere quando si usano librerie di terze parti scarsamente documentate perché è necessario conoscere la gamma di valori possibili per gli argomenti per passare il valore corretto.

Alcuni degli scenari comuni in cui si verifica RangeError sono:

  • Cercare di creare un array di lunghezza non ammessa tramite il costruttore Array.
  • Passare valori errati a metodi numerici come toExponential(), toPrecision(), toFixed(), ecc.
  • Passare valori non ammessi a funzioni di stringa come normalize().

ReferenceError

Un ReferenceError si verifica quando qualcosa va storto con il riferimento di una variabile nel vostro codice. Potreste aver dimenticato di definire un valore per la variabile prima di utilizzarla, o potreste cercare di usare una variabile inaccessibile nel vostro codice. In ogni caso, l’analisi dello stack trace fornisce ampie informazioni per trovare e correggere il riferimento alla variabile che dà errore.

Alcune delle ragioni comuni per cui si verificano i ReferenceErrors sono:

  • Fare un errore di battitura in un nome di variabile.
  • Cercare di accedere a variabili block-scoped al di fuori dei loro scopi.
  • Fare riferimento a una variabile globale da una libreria esterna (come $ da jQuery) prima che sia caricata.

SyntaxError

Questo tipo di errori è tra i più semplici da risolvere perché indicano un errore nella sintassi del codice. Poiché JavaScript è un linguaggio di scripting interpretato piuttosto che compilato, questi errori compaiono quando l’applicazione esegue lo script che contiene l’errore. Nel caso dei linguaggi compilati, tali errori vengono identificati durante la compilazione. Pertanto, i binari dell’app non vengono creati fino a quando questi non vengono corretti.

Alcune delle ragioni comuni per cui i SyntaxErrors potrebbero verificarsi sono:

  • Virgolette mancanti.
  • Parentesi di chiusura mancanti.
  • Allineamento improprio delle parentesi graffe o di altri caratteri.

È una buona pratica usare uno strumento di linting nel vostro IDE per identificare tali errori prima che arrivino al browser.

TypeError

TypeError è uno degli errori più comuni nelle applicazioni JavaScript. Questo errore viene creato quando qualche valore non risulta essere di un particolare tipo previsto. Alcuni dei casi comuni in cui si verifica sono:

  • Invocazione di oggetti che non sono metodi.
  • Tentare di accedere a proprietà di oggetti nulli o indefiniti
  • Trattare una stringa come un numero o viceversa

Ci sono molte altre possibilità in cui un TypeError può verificarsi. Vedremo più tardi alcuni casi famosi e impareremo come risolverli.

InternalError

Il tipo InternalError viene utilizzato quando si verifica un’eccezione nel motore di runtime JavaScript. Può indicare o meno un problema con il vostro codice.

Più spesso, InternalError si verifica solo in due scenari:

  • Quando una patch o un aggiornamento del runtime JavaScript porta un bug che lancia eccezioni (questo accade molto raramente).
  • Quando il vostro codice contiene entità che sono troppo grandi per il motore JavaScript (per esempio troppi casi di switch, inizializzatori di array troppo grandi, troppa ricorsività).

L’approccio più appropriato per risolvere questo errore è quello di identificare la causa attraverso il messaggio di errore e ristrutturare la logica della vostra app, se possibile, per eliminare l’improvviso picco di carico di lavoro sul motore JavaScript.

URIError

URIError si verifica quando si usa una funzione globale di gestione degli URI come decodeURIComponent ma questa non è ammessa. Di solito indica che il parametro passato alla chiamata di metodo non è conforme agli standard URI e quindi non è stato analizzato correttamente dal metodo.

La diagnosi di questi errori di solito è facile dato che avete solo bisogno di esaminare gli argomenti per cercare la malformazione.

EvalError

Un EvalError si verifica in caso di errore con una chiamata alla funzione eval(). La funzione eval() è utilizzata per eseguire il codice JavaScript memorizzato nelle stringhe. Tuttavia, poiché l’utilizzo della funzione eval() è altamente sconsigliato a causa di problemi di sicurezza e le attuali specifiche ECMAScript non lanciano più la classe EvalError, questo tipo di errore è rimasto semplicemente per mantenere la compatibilità all’indietro con il legacy code JavaScript.

Se state lavorando su una vecchia versione di JavaScript, potreste incontrare questo errore. In ogni caso, è meglio indagare il codice eseguito nella chiamata di funzione eval() per qualsiasi eccezione.

Creare Tipi di Errore Personalizzati

Anche se JavaScript offre una lista adeguata di classi di tipi di errore per coprire la maggior parte degli scenari, potete sempre creare un nuovo tipo di errore se la lista non soddisfa le vostre esigenze. La base di questa flessibilità sta nel fatto che JavaScript vi permette di lanciare letteralmente qualsiasi cosa con il comando throw.

Quindi, tecnicamente, queste dichiarazioni sono del tutto regolari:

throw 8
throw "An error occurred"

Tuttavia, lanciare un tipo di dati primitivo non fornisce dettagli sull’errore, come il tipo, il nome o lo stack trace che lo accompagna. Per risolvere questo problema e standardizzare il processo di gestione degli errori, è stata fornita la classe Error. È anche sconsigliato usare tipi di dati primitivi mentre si lanciano le eccezioni.

Potete estendere la classe Error per creare la vostra classe di errore personalizzata. Ecco un esempio di base di come potete farlo:

class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = "ValidationError";
    }
}

E potete usarla nel seguente modo:

throw ValidationError("Property not found: name")

Potete identificarlo usando la parola chiave instanceof:

try {
    validateForm() // code that throws a ValidationError
} catch (e) {
    if (e instanceof ValidationError)
    // do something
    else
    // do something else
}

I 10 Errori Più Comuni in JavaScript

Ora che avete capito i tipi di errori comuni e come creare quelli personalizzati, è il momento di guardare alcuni degli errori più comuni che incontrerete quando scrivete codice JavaScript.

Date un’Occhiata alla Nostra Video-Guida agli Errori JavaScript Più Comuni

1. Uncaught RangeError

Questo errore si verifica in Google Chrome in scenari diversi. In primo luogo, può accadere se chiamate una funzione ricorsiva e questa non termina. Potete verificarlo voi stessi nella Chrome DevTools:

L’errore “Uncaught RangeError: Maximum call stack size exceeded” è mostrato su uno sfondo rosso accanto all'icona di una croce rossa con sopra il codice di una funzione ricorsiva
Esempio di RangeError con una chiamata di funzione ricorsiva.

Quindi per risolvere questo errore, assicuratevi di definire correttamente i casi limite della vostra funzione ricorsiva. Un’altra ragione per cui questo errore si verifica è se avete passato un valore che è fuori dall’intervallo del parametro di una funzione. Ecco un esempio:

L’errore “Uncaught RangeError: toExponential() argument must be between 0 and 100” è mostrato su uno sfondo rosso accanto all'icona di una croce rossa con una chiamata alla funzione toExponential() sopra di esso.
Esempio di RangeError con la chiamata toExponential().

Il messaggio di errore di solito indica cosa c’è di sbagliato nel vostro codice. Una volta apportate le modifiche, verrà risolto.

num = 4. num.toExponential(2). Output: 4.00e+0.
Output per la chiamata alla funzione toExponential().

2. Uncaught TypeError: Cannot Set Property

Questo errore si verifica quando impostate una proprietà su un riferimento non definito. Potete riprodurre il problema con questo codice:

var list
list.count = 0

Ecco l’output che riceverete:

L’errore “Uncaught TypeError: Cannot set properties of undefined” è mostrato su uno sfondo rosso accanto a un'icona a forma di croce rossa con un'assegnazione list.count = 0 sopra di esso
Esempio di TypeError.

Per risolvere questo errore, inizializzate il riferimento con un valore prima di accedere alle sue proprietà. Ecco come appare una volta risolto:

Impostazione di list.count = 10 dopo l’inizializzazione della lista con {} che dà come outuput 10.
Come risolvere TypeError.

3. Uncaught TypeError: Cannot Read Property

Questo è uno degli errori più frequenti in JavaScript: si verifica quando si tenta di leggere una proprietà o chiamare una funzione su un oggetto non definito. Potete riprodurlo molto facilmente eseguendo il seguente codice nella console di Chrome Developer:

var func
func.call()

Ecco l’output:

L’errore“Uncaught TypeError: Cannot read properties of undefined” è mostrato su uno sfondo rosso accanto all'icona di una croce rossa con func.call() sopra di esso.
Esempio di TypeError con funzione non definita.

Un oggetto non definito è una delle tante possibili cause di questo errore. Un’altra causa importante di questo problema può essere un’inizializzazione impropria dello stato durante il rendering dell’UI. Ecco un esempio reale di un’applicazione React:

import React, { useState, useEffect } from "react";

const CardsList = () => {

    const [state, setState] = useState();

    useEffect(() => {
        setTimeout(() => setState({ items: ["Card 1", "Card 2"] }), 2000);
    }, []);

    return (
        <>
            {state.items.map((item) => (
                <li key={item}>{item}</li>
            ))}
        </>
    );
};

export default CardsList;

L’applicazione inizia con un contenitore di stato vuoto e riceve alcuni elementi dopo un ritardo di 2 secondi. Il ritardo è messo in atto per imitare una chiamata di rete. Anche se la vostra rete è super veloce, dovrete comunque affrontare un piccolo ritardo a causa del quale il componente verrà renderizzato almeno una volta. Se provate a eseguire questa applicazione, riceverete il seguente errore:

L’errore “undefined is not an object” è mostrato su uno sfondo grigio.
TypeError stack trace in un browser.

Questo perché, al momento del rendering, il contenitore di stato è indefinito; quindi non esiste alcuna proprietà items su di esso. Risolvere questo errore è facile. Avete solo bisogno di fornire un valore predefinito iniziale al contenitore di stato.

// ...
const [state, setState] = useState({items: []});
// ...

Ora, dopo il ritardo impostato, la vostra app mostrerà un output simile:

Una lista puntata con due elementi, "Card 1" e "Card 2".
Output del codice.

La correzione esatta nel vostro codice potrebbe essere diversa, ma il fulcro qui è inizializzare sempre correttamente le vostre variabili prima di usarle.

4. TypeError: ‘undefined’ Is Not an Object

Questo errore si verifica in Safari quando cercate di accedere alle proprietà o chiamare un metodo su un oggetto non definito. Potete eseguire lo stesso codice di cui sopra per riprodurre l’errore da solo.

L’errore “TypeError: undefined is not an object” mostrato su uno sfondo rosso accanto a un'icona rossa a forma di punto esclamativo con sopra la chiamata al metodo func.call().
Esempio di TypeError con funzione non definita.

Anche la soluzione a questo errore è la stessa: assicuratevi di aver inizializzato correttamente le vostre variabili e che non siano indefinite quando accedete a una proprietà o metodo.

5. TypeError: null Is Not an Object

Questo è simile all’errore precedente. Si verifica su Safari e l’unica differenza tra i due errori è che questo viene lanciato quando l’oggetto alla cui proprietà o metodo si sta accedendo è null invece di undefined. Potete riprodurlo eseguendo il codice seguente:

var func = null

func.call()

Ecco l’output che riceverete:

Il messaggio di errore
Esempio di TypeError con funzione null.

null è un valore impostato esplicitamente su una variabile e non assegnato automaticamente da JavaScript. Questo errore può verificarsi solo se state cercando di accedere a una variabile che avete impostato voi su null. Quindi, dovete rivedere il vostro codice e controllare se la logica che avete scritto è corretta o no.

6. TypeError: Cannot Read Property ‘length’

Questo errore si verifica in Chrome quando cercate di leggere la lunghezza di un oggetto null o undefined. La causa di questo problema è simile a quelli già visti, ma si verifica abbastanza frequentemente durante la gestione delle liste; quindi merita una menzione speciale. Ecco come potete riprodurre il problema:

L’errore “Uncaught TypeError: Cannot read property 'length' of undefined” mostrato su uno sfondo rosso accanto all'icona di una croce rossa con la chiamata myButton.length sopra di esso.
Esempio di TypeError con un oggetto non definito.

Tuttavia, nelle versioni più recenti di Chrome, questo errore viene riportato come Uncaught TypeError: Cannot read properties of undefined. Ecco come appare ora:

L’errore “Uncaught TypeError: Cannot read properties of undefined” mostrato su uno sfondo rosso accanto all'icona di una croce rossa con la chiamata myButton.length sopra di esso.
Esempio di TypeError con un oggetto non definito nelle versioni più recenti di Chrome.

La correzione, ancora una volta, consiste nell’assicurarsi che l’oggetto alla cui lunghezza state cercando di accedere esista e non sia impostato su null.

7. TypeError: ‘undefined’ Is Not a Function

Questo errore si verifica quando cercate di richiamare un metodo che non esiste nel vostro script, oppure esiste ma non può essere referenziato nel contesto chiamante. Questo errore di solito si verifica in Google Chrome e potete risolverlo controllando la linea di codice che lancia l’errore. Se trovate un errore di battitura, correggetelo e controllate se questo risolve il vostro problema.

Se avete usato la parola chiave di autoreferenziazione this nel vostro codice, questo errore potrebbe verificarsi se this non è legata in modo appropriato al vostro contesto. Considerate il seguente codice:

function showAlert() {
    alert("message here")
}

document.addEventListener("click", () => {
    this.showAlert();
})

Se eseguite il codice qui sopra, verrà lanciato l’errore di cui abbiamo parlato. Succede perché la funzione anonima passata come event listener viene eseguita nel contesto di document.

Al contrario, la funzione showAlert è definita nel contesto di window.

Per risolvere questo problema, dovete passare il riferimento corretto alla funzione legandolo con il metodo bind():

document.addEventListener("click", this.showAlert.bind(this))

8. ReferenceError: Event Is Not Defined

Questo errore si verifica quando cercate di accedere a un riferimento non definito nell’ambito chiamante. Questo di solito accade quando si gestiscono gli eventi perché spesso vi forniscono un riferimento chiamato event nella funzione di callback. Questo errore può verificarsi se dimenticate di definire l’argomento dell’evento nei parametri della vostra funzione o se lo scrivete male.

Questo errore potrebbe non verificarsi in Internet Explorer o Google Chrome (dato che IE offre una variabile di evento globale e Chrome attacca la variabile di evento automaticamente al gestore), ma può verificarsi in Firefox. Quindi è consigliabile tenere d’occhio questi piccoli errori.

9. TypeError: Assignment to Constant Variable

Questo è un errore che si verifica per disattenzione. Se provate ad assegnare un nuovo valore a una variabile costante, vi ritroverete con questo risultato:

L’errore “Uncaught TypeError: Assignment to constant variable” mostrato su uno sfondo rosso accanto all'icona di una croce rossa con assegnazione func = 6 sopra di essa.
Esempio di TypeError con assegnazione a oggetto costante.

Anche se sembra facile da risolvere in questo momento, immaginate centinaia di dichiarazioni di variabili di questo tipo e una di esse definita erroneamente come const invece di let! A differenza di altri linguaggi di scripting come PHP, c’è una differenza minima tra lo stile di dichiarazione delle costanti e delle variabili in JavaScript. Pertanto vi consigliamo di controllare le vostre dichiarazioni prima di tutto quando vi trovate di fronte a questo errore. Potreste anche incorrere in questo errore se dimenticate che il suddetto riferimento è una costante e lo usate come variabile. Questo indica una disattenzione o un difetto nella logica della vostra applicazione. Assicuratevi di controllarlo quando cercate di risolvere questo problema.

10. (unknown): Script Error

Uno script error si verifica quando uno script di terze parti invia un errore al vostro browser. Questo errore è seguito da (unknown) perché lo script di terze parti appartiene a un dominio diverso dalla vostra applicazione. Il browser nasconde altri dettagli per evitare la fuga di informazioni sensibili dallo script di terze parti.

Non potete risolvere questo errore senza conoscere i dettagli completi. Ecco cosa potete fare per ottenere maggiori informazioni sull’errore:

  1. Aggiungete l’attributo crossorigin nel tag script.
  2. Impostate l’intestazione corretta Access-Control-Allow-Origin sul server che ospita lo script.
  3. [Opzionale] Se non avete accesso al server che ospita lo script, potete considerare di utilizzare un proxy per inoltrare la vostra richiesta al server e tornare al client con le intestazioni corrette.

Una volta che potete accedere ai dettagli dell’errore, potete poi mettervi a sistemare il problema, che probabilmente sarà o con la libreria di terze parti o con la rete.

Come Identificare e Prevenire gli Errori in JavaScript

Anche se gli errori discussi sopra sono i più comuni e frequenti in cui vi imbatterete in JavaScript, basarsi su alcuni esempi non sarà mai essere sufficiente. È fondamentale capire come individuare e prevenire qualsiasi tipo di errore in un’applicazione JavaScript mentre la si sviluppa. Ecco come potete gestire gli errori in JavaScript.

Lanciare e ‘Catturare’ Manualmente gli Errori

Il modo base per gestire gli errori che sono stati lanciati manualmente o dal runtime è quello di ‘catturarli’. Come molti altri linguaggi, JavaScript offre una serie di parole chiave per gestire gli errori. È essenziale conoscere a fondo ognuna di esse prima di iniziare a gestire gli errori nella vostra applicazione JavaScript.

throw

La prima e più elementare parola chiave dell’insieme è throw, che in inglese significa lanciare. Come evidente, la parola chiave throw è utilizzata per lanciare errori per creare eccezioni nel runtime JavaScript manualmente. Ne abbiamo già parlato prima ed ecco il succo del significato di questa parola chiave:

  • Potete usare throw con qualsiasi cosa, compresi i numeri, le stringhe e gli oggetti Error.
  • Tuttavia, non vi consigliamo di lanciare tipi di dati primitivi come stringhe e numeri poiché non portano informazioni di debug sugli errori.
  • Esempio: throw TypeError("Please provide a string")

try

La parola chiave try è utilizzata per indicare che un blocco di codice potrebbe lanciare un’eccezione. La sua sintassi è:

try {
    // error-prone code here
}

È importante notare che un blocco catch deve sempre seguire il blocco try per gestire efficacemente gli errori.

catch

La parola chiave catch è utilizzata per creare un blocco catch. Questo blocco di codice è responsabile della gestione degli errori che il blocco finale try cattura. Ecco la sua sintassi:

catch (exception) {
    // code to handle the exception here
}

E questo è il modo in cui si implementano i blocchi try e catch insieme:

try {
    // business logic code
} catch (exception) {
    // error handling code
}

A differenza di C++ o Java, non potete aggiungere più blocchi catch a un blocco try in JavaScript. Questo significa che non potete fare questo:

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    }
} catch (exception) {
    if (exception instanceof RangeError) {
    // do something
    }
}

Potete invece usare un’istruzione if...else o un’istruzione switch case all’interno del singolo blocco catch per gestire tutti i possibili casi di errore. Sarebbe come questo:

try {
    // business logic code
} catch (exception) {
    if (exception instanceof TypeError) {
        // do something
    } else if (exception instanceof RangeError) {
        // do something else
    }
}

finally

La parola chiave finally si usa per definire un blocco di codice che viene eseguito dopo che un errore è stato gestito. Questo blocco viene eseguito dopo i blocchi try e catch.

Inoltre, il blocco finally viene eseguito indipendentemente dal risultato degli altri due blocchi. Questo significa che anche se il blocco catch non può gestire completamente l’errore o se viene lanciato un errore nel blocco catch, l’interprete eseguirà il codice nel blocco finally prima che il programma vada in crash.

Per essere considerato valido, il blocco try in JavaScript deve essere seguito da un blocco catch o un blocco finally. Senza uno di questi, l’interprete solleverà un SyntaxError. Pertanto, assicuratevi di seguire i vostri blocchi try con almeno uno dei due quando gestite gli errori.

Gestire gli Errori a Livello Globale con il Metodo onerror()

Il metodo onerror() è disponibile per tutti gli elementi HTML e serve a gestire qualsiasi errore che può verificarsi con essi. Per esempio, se un tag img non riesce a trovare l’immagine il cui URL è specificato, questo lancia il suo metodo onerror per permettere all’utente di gestire l’errore.

Di solito dovreste fornire un altro URL dell’immagine nella chiamata onerror per il tag img. Ecco come potete farlo tramite JavaScript:

const image = document.querySelector("img")

image.onerror = (event) => {
    console.log("Error occurred: " + event)
}

Tuttavia, potete usare questa funzione per creare un meccanismo globale di gestione degli errori per la vostra applicazione. Ecco come potete farlo:

window.onerror = (event) => {
    console.log("Error occurred: " + event)
}

Con questo gestore di eventi, potete sbarazzarvi dei molteplici blocchi try...catch sparsi nel vostro codice e centralizzare la gestione degli errori della vostra applicazione in modo simile alla gestione degli eventi. Potete attaccare più gestori di errori alla finestra per mantenere il principio di responsabilità unica dai principi di progettazione SOLID. L’interprete passerà ciclicamente attraverso tutti i gestori fino a raggiungere quello appropriato.

Passare gli Errori Tramite Callback

Mentre le funzioni semplici e lineari permettono alla gestione degli errori di rimanere semplice, le callback possono complicare la faccenda.

Considerate il codice seguente:

const calculateCube = (number, callback) => {
    setTimeout(() => {
        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

calculateCube(4, callback)

La funzione qui sopra dimostra una condizione asincrona in cui una funzione impiega del tempo per elaborare le operazioni e restituisce il risultato più tardi con l’aiuto di una callback.

Se provate a inserire una stringa invece di 4 nella chiamata della funzione, otterrete NaN come risultato.

Questo deve essere gestito correttamente. Ecco come:

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number")
            throw new Error("Numeric argument is expected")

        const cube = number * number * number
        callback(cube)
    }, 1000)
}

const callback = result => console.log(result)

try {
    calculateCube(4, callback)
} catch (e) { console.log(e) }

Questo in teoria dovrebbe risolvere il problema. Tuttavia, se provate a passare una stringa alla chiamata della funzione, riceverete questo:

L’errore “Uncaught Error: Numeric argument is expected” mostrato su uno sfondo rosso scuro accanto all'icona di una croce rossa.
Esempio di errore con argomento sbagliato.

Anche se avete implementato un blocco try-catch mentre chiamate la funzione, dice che l’errore non è stato gestito, è ancora uncaught. L’errore viene lanciato dopo che il blocco catch è stato eseguito a causa del ritardo del timeout.

Questo può verificarsi rapidamente nelle chiamate di rete, dove si insinuano ritardi imprevisti. Dovete occuparvi di questi casi mentre sviluppate la vostra applicazione.

Ecco come potete gestire correttamente gli errori nelle callback:

const calculateCube = (number, callback) => {

    setTimeout(() => {
        if (typeof number !== "number") {
            callback(new TypeError("Numeric argument is expected"))
            return
        }
        const cube = number * number * number
        callback(null, cube)
    }, 2000)
}

const callback = (error, result) => {
    if (error !== null) {
        console.log(error)
        return
    }
    console.log(result)
}

try {
    calculateCube('hey', callback)
} catch (e) {
    console.log(e)
}

Ora, l’output nella console sarà:

Il messaggio “TypeError: Numeric argument is expected” mostrato su uno sfondo grigio scuro
Esempio di TypeError con argomento non ammesso.

Questo indica che l’errore è stato gestito in modo appropriato.

Gestire gli Errori nelle Promises

La maggior parte delle persone tende a preferire le promises per gestire le attività asincrone. Le promises hanno un altro vantaggio: una promise rifiutata non termina il vostro script. Tuttavia, avete ancora bisogno di implementare un blocco catch per gestire gli errori nelle promises. Per capire meglio a cosa ci riferiamo, riscriviamo la funzione calculateCube() usando le promises:

const delay = ms => new Promise(res => setTimeout(res, ms));

const calculateCube = async (number) => {
    if (typeof number !== "number")
        throw Error("Numeric argument is expected")
    await delay(5000)
    const cube = number * number * number
    return cube
}

try {
    calculateCube(4).then(r => console.log(r))
} catch (e) { console.log(e) }

Il timeout del codice precedente è stato isolato nella funzione delay per la comprensione. Se provate a inserire una stringa invece di 4, l’output che otterrete sarà simile a questo:

L’errore “Uncaught (in promise) Error: Numeric argument is expected” mostrato su uno sfondo grigio scuro accanto all'icona di una croce rossa
Esempio di TypeError con un argomento non ammesso in Promise.

Di nuovo, questo è dovuto al fatto che Promise lancia l’errore dopo che tutto il resto ha completato l’esecuzione. La soluzione a questo problema è semplice. Aggiungete semplicemente una chiamata catch() alla catena delle promises come questa:

calculateCube("hey")
.then(r => console.log(r))
.catch(e => console.log(e))

Ora l’output sarà:

Il messaggio “Error: Numeric argument is expected” mostrato su uno sfondo grigio scuro
Esempi di gestione di un TypeError con argomento non ammesso.

Come vedete è abbastanza facile gestire gli errori con le promises. Inoltre, potete concatenare un blocco finally() e la chiamata alla promise per aggiungere codice che verrà eseguito dopo che la gestione dell’errore è stata completata.

In alternativa, potete anche gestire gli errori nelle promises usando la tradizionale tecnica try-catch-finally. Ecco come sarebbe la vostra chiamata a promise in questo caso:

try {
    let result = await calculateCube("hey")
    console.log(result)
} catch (e) {
    console.log(e)
} finally {
    console.log('Finally executed")
}

Tuttavia, questo funziona solo all’interno di una funzione asincrona. Quindi il modo migliore per gestire gli errori nelle promises è quello di concatenare catch e finally alla chiamata della promise.

throw/catch vs onerror() vs Callback vs Promises: Qual È il Migliore?

Avendo quattro metodi a disposizione, dovete sapere quale scegliere il più appropriato a seconda del caso d’uso. Ecco come potete decidere:

throw/catch

Userete questo metodo la maggior parte delle volte. Assicuratevi di implementare le condizioni per tutti i possibili errori all’interno del vostro blocco catch, e ricordatevi di includere un blocco finally se avete bisogno di eseguire alcune routine di pulizia della memoria dopo il blocco try.

Tuttavia, troppi blocchi try/catch possono rendere il vostro codice difficile da mantenere. Se vi trovate in una situazione del genere, potreste voler gestire gli errori tramite il gestore globale o il metodo promise.

Quando si decide tra i blocchi asincroni try/catch e la promise catch(), è consigliabile optare per i blocchi asincroni try/catch perché renderanno il vostro codice lineare e facile da eseguire il debug.

onerror()

È meglio usare il metodo onerror() quando sapete che la vostra applicazione deve gestire molti errori e questi possono essere sparsi lungo tutto il codice. Il metodo onerror vi permette di gestire gli errori come se fossero un altro evento gestito dalla vostra applicazione. Potete definire più gestori di errori e inserirli nella finestra della vostra applicazione durante il rendering iniziale.

Tuttavia, dovete anche ricordare che il metodo onerror() può essere inutilmente impegnativo da impostare in progetti più piccoli con una minore portata di errore. Se avete la certezza che la vostra app non lancerà troppi errori, il metodo tradizionale throw/catch funzionerà meglio per voi.

Callback e Promises

La gestione degli errori nei callback e nelle promises differisce a causa del design e della struttura del codice. Tuttavia, se scegliete tra questi due prima di aver scritto il vostro codice, sarebbe meglio optare per le promises.

Questo perché le promises hanno un costrutto incorporato per concatenare un catch() e un blocco finally() per gestire facilmente gli errori. Questo metodo è più facile e pulito che definire argomenti aggiuntivi/riutilizzare argomenti esistenti per gestire gli errori.

Tenere Traccia dei Cambiamenti con i Repository Git

Molti errori sorgono spesso a causa di errori manuali nel codice. Durante lo sviluppo o il debug del vostro codice, potreste finire per fare cambiamenti non necessari che potrebbero causare nuovi errori nella vostra codebase. I test automatici sono un ottimo modo per tenere sotto controllo il vostro codice dopo ogni cambiamento. Tuttavia, può solo dirvi se qualcosa è sbagliato. Se non fate frequenti backup del vostro codice, finirete per perdere tempo cercando di sistemare una funzione o uno script che prima funzionava benissimo.

È qui che Git gioca la sua parte. Con una corretta strategia di commit, potete usare la vostra cronologia git come un sistema di backup per vedere in che modo si è evoluto il vostro codice durante lo sviluppo. Potete facilmente sfogliare i vostri vecchi commit e scoprire la versione della funzione che prima funzionava bene ma che ha dato errori dopo un cambiamento non correlato.

Potete quindi ripristinare il vecchio codice o confrontare le due versioni per determinare cosa è andato storto. I moderni strumenti di sviluppo web come GitHub Desktop o GitKraken vi aiutano a visualizzare questi cambiamenti fianco a fianco e a capire gli errori rapidamente.

Un’abitudine che può aiutarvi a fare meno errori è quella di eseguire revisioni del codice ogni volta che fate un cambiamento significativo al vostro codice. Se state lavorando in un team, potete creare una richiesta di pull e farla revisionare da una persona del team. Questo vi aiuterà a fidarvi di un secondo paio di occhi per individuare eventuali errori che potrebbero esservi sfuggiti.

Migliori Pratiche per Gestire gli Errori in JavaScript

I metodi sopra menzionati sono l’ideale per progettare un robusto approccio di gestione degli errori per la vostra prossima applicazione JavaScript. Tuttavia, sarebbe meglio tenere alcune cose in mente mentre li implementate per ottenere il meglio dalla vostra gestione degli errori. Ecco alcuni suggerimenti.

1. Usare gli Errori Personalizzati Quando Si Gestiscono le Eccezioni Operative

Abbiamo introdotto gli errori personalizzati all’inizio di questa guida per darvi un’idea di come personalizzare la gestione degli errori per il caso unico della vostra applicazione. È consigliabile usare gli errori personalizzati quando possibile invece della classe generica Error in quanto fornisce più informazioni contestuali all’ambiente chiamante sull’errore.

Inoltre, gli errori personalizzati vi permettono di moderare il modo in cui un errore viene mostrato all’ambiente chiamante. Questo significa che potete scegliere di nascondere dettagli specifici o mostrare informazioni aggiuntive sull’errore come e quando volete.

Potete arrivare a formattare il contenuto dell’errore secondo le vostre esigenze. Questo vi dà un migliore controllo su come l’errore viene interpretato e gestito.

2. Non Ammettere Nessuna Eccezione

Anche le persone più esperte di sviluppo spesso fanno un errore da principianti: consumare tutti i livelli di eccezioni nel corso del codice.

Potreste imbattervi in situazioni in cui avete un pezzo di codice da eseguire in modo opzionale. Se funziona, bene; se non funziona, non c’è bisogno di fare nulla.

In questi casi, si è spesso tentati di mettere questo codice in un blocco try e allegarvi un blocco catch vuoto. Tuttavia, facendo così lascerete quel pezzo di codice libero di causare qualsiasi tipo di errore e farla franca. Questo può diventare pericoloso se avete una codebase grande e molte istanze di questi costrutti di gestione degli errori.

Il modo migliore per gestire le eccezioni è quello di determinare un livello in cui saranno trattate tutte e raccoglierle fino a quel punto. Questo livello può essere un controller (in un’app con architettura MVC) o un middleware (in un’app tradizionale orientata al server).

In questo modo saprete dove trovare tutti gli errori che si verificano nella vostra app e scegliere come risolverli, anche se questo significa non fare nulla.

3. Usare una Strategia Centralizzata per i Log e gli Avvisi di Errore

La registrazione di un errore è spesso parte integrante della sua gestione. Coloro che non riescono a sviluppare una strategia centralizzata per la registrazione degli errori possono perdere informazioni preziose sull’utilizzo della loro applicazione.

I registri degli eventi di un’app possono aiutarvi a capire dati cruciali sugli errori e aiutarvi a fare il debug rapidamente. Se avete dei meccanismi di allarme adeguati impostati nella vostra app, potete sapere quando si verifica un errore nella vostra app prima che raggiunga una grande parte della vostra base di utenti.

È consigliabile usare un logger pre-costruito o crearne uno per soddisfare le vostre esigenze. Potete configurare questo logger per gestire gli errori in base ai loro livelli (warning, debug, info, ecc.) e alcuni logger arrivano addirittura a inviare immediatamente i log ai server di registrazione remoti. In questo modo, potete osservare come la logica della vostra applicazione si comporta con gli utenti attivi.

4. Notificare agli Utenti gli Errori in Modo Appropriato

Un altro buon punto da ricordare quando definite la vostra strategia di gestione degli errori è quello di tenere sempre a mente l’utente.

Tutti gli errori che interferiscono con il normale funzionamento della vostra applicazione devono presentare un avviso visibile per notificare agli utenti che qualcosa è andato storto, in modo che loro possano provare a trovare una soluzione. Se conoscete una soluzione rapida per l’errore, come riprovare un’operazione o fare il logout e il login, assicuratevi di menzionarla nel messaggio di avviso per aiutare a risolvere l’esperienza dell’utente in tempo reale.

Nel caso di errori che non causano alcuna interferenza con l’esperienza quotidiana dell’utente, potete considerare di sopprimere l’avviso e registrare l’errore su un server remoto per risolverlo in seguito.

5. Implementare un Middleware (Node.js)

L’ambiente Node.js supporta i middleware per aggiungere funzionalità alle applicazioni server. Potete usare questa caratteristica per creare un middleware di gestione degli errori per il vostro server.

Il vantaggio più significativo dell’utilizzo del middleware è che tutti i vostri errori sono gestiti centralmente in un unico posto. Potete scegliere di attivare/disattivare questa configurazione per scopi di test facilmente.

Ecco come potete creare un middleware di base:

const logError = err => {
    console.log("ERROR: " + String(err))
}

const errorLoggerMiddleware = (err, req, res, next) => {
    logError(err)
    next(err)
}

const returnErrorMiddleware = (err, req, res, next) => {
    res.status(err.statusCode || 500)
       .send(err.message)
}

module.exports = {
    logError,
    errorLoggerMiddleware,
    returnErrorMiddleware
}

Potete poi usare questo middleware nella vostra app in questo modo:

const { errorLoggerMiddleware, returnErrorMiddleware } = require('./errorMiddleware')

app.use(errorLoggerMiddleware)

app.use(returnErrorMiddleware)

Ora potete definire una logica personalizzata all’interno del middleware per gestire gli errori in modo appropriato. Non avete più bisogno di preoccuparvi di implementare singoli costrutti di gestione degli errori in tutto il vostro codice.

6. Riavviare la Vostra App per Gestire gli Errori di Programmazione (Node.js)

Quando le app Node.js incontrano errori di programmazione, potrebbero non lanciare un’eccezione e provare a chiudere l’app. Tali errori possono includere problemi derivanti da errori di programmazione, come l’alto consumo di CPU, riempimento della memoria o le perdite di memoria. Il modo migliore per gestirli è quello di riavviare l’app mandandola in crash tramite la modalità cluster di Node.js o uno strumento unico come PM2. Questo può garantire che l’app non vada in crash su azione dell’utente, presentando un’esperienza utente terribile.

7. Trovare Tutte le Eccezioni Non Contemplate (Node.js)

Non avrete mai la certezza di aver pensato a ogni possibile errore che può verificarsi nella vostra app. Pertanto, è essenziale implementare una strategia di ripiego per trovare tutte le eccezioni non contemplate dalla vostra app.

Ecco come potete farlo:

process.on('uncaughtException', error => {
    console.log("ERROR: " + String(error))
    // other handling mechanisms
})

Potete anche capire se l’errore che si è verificato è un’eccezione standard o un errore operativo personalizzato. In base al risultato, potete uscire dal processo e riavviarlo per evitare un comportamento inaspettato.

8. Trovare Tutti i Casi Non Gestiti di Promises Respinte (Node.js)

Così come non potrete mai pensare in anticipo a tutte le possibili eccezioni, c’è un’alta possibilità di non gestire tutti le promises respinte. Tuttavia, a differenza delle eccezioni, le promises respinte non generano errori.

Quindi, una promise importante che viene respinta potrebbe sfuggire e lasciare la vostra applicazione aperta alla possibilità di incorrere in un comportamento inaspettato. Pertanto, è fondamentale implementare un meccanismo di fallback per gestire le promises respinte.

Ecco come potete farlo:

const promiseRejectionCallback = error => {
    console.log("PROMISE REJECTED: " + String(error))
}

process.on('unhandledRejection', callback)

Riepilogo

Come capita con qualsiasi altro linguaggio di programmazione, gli errori sono abbastanza frequenti e naturali in JavaScript. In alcuni casi, potreste anche aver bisogno di lanciare errori intenzionalmente per indicare la risposta corretta ai vostri utenti. Quindi, capire la loro anatomia e le varie tipologie di errore è cruciale.

Inoltre, dovete avere gli strumenti e le tecniche giuste per identificare ed evitare che gli errori distruggano la vostra applicazione.

Nella maggior parte dei casi, una solida strategia per gestire gli errori con un’attenta esecuzione è sufficiente per tutti i tipi di applicazioni JavaScript.

Ci sono altri errori JavaScript che non ancora non hai potuto risolvere? Avete tecniche da consigliare per gestire gli errori JS in modo costruttivo? Fatecelo sapere nei commenti qui sotto!

Kumar Harsh

Kumar è software developer e autore tecnico con sede in India. È specializzato in JavaScript e DevOps. Per saperne di più sul suo lavoro, visitate il suo sito web.