Un'eccezione è il verificarsi di una condizione anormale nel flusso di esecuzione di un programma. La gestione delle eccezioni è un requisito fondamentale nello sviluppo con Node.js.

In Node.js le eccezioni provocano la terminazione immediata nell'esecuzione di un'applicazione. Le cause più comuni delle eccezioni possono essere le seguenti:

  1. Errori sintattici.
  2. Errori di riferimento (undefined o null).
  3. Errori sollevati dalle funzioni di callback.
  4. Errori sollevati dalle Promise e non gestiti.
  5. Errori HTTP.
  6. Errori sollevati dai moduli NPM.

Gli errori sintattici possono essere risolti a priori abilitando la verifica sintattica sull'IDE o sull'editor di sviluppo. Si tratta degli errori di più immediata risoluzione.

Gli errori di riferimento avvengono o quando una variabile è irraggiungibile dallo scope corrente o quando si cerca di accedere ad una proprietà e ad un metodo di un oggetto che non esistono nell'oggetto in questione.

Per verificare l'esistenza di una proprietà possiamo far ricorso all'operatore typeof.

if(typeof obj.property !== 'undefined') {
    //...
}

Un approccio alternativo riguarda l'uso dell'operatore in.

if('property' in obj) {
    //...
}

Poichè JavaScript è un linguaggio debolmente tipizzato, verificare solo il valore undefined spesso non è sufficiente e occorre specificare il tipo di dati che ci aspettiamo.

if(typeof post.id === 'number') {
    //...
}

null non richiede l'uso dell'operatore typeof in quanto restituirebbe un valore incoerente (object) ma più semplicemente l'operatore di identità.

if(post === null) {
    //...
}

Un caso particolare di questo tipo di errori è molto frequente quando accettiamo l'input da parte dell'utente. Supponiamo che l'utente inserisca un prezzo in formato decimale e la nostra applicazione debba restituire una stringa formattata.

router.post('/price', (req, res, next) => {
    const { price } = req.body;
    const formattedPrice = parseFloat(price).toFixed(2);
    res.json({ formattedPrice });
});

Se l'utente inserisce una stringa vuota, parseFloat() restituirà NaN che a sua volta genererà un errore nel metodo toFixed() in quanto questo metodo non esiste in NaN. La soluzione in questi casi consiste nel validare l'input utente prima di usarlo per le nostre operazioni.

Gli errori generati dalle funzioni di callback riguardano quei metodi dei moduli core di Node.js o dei moduli NPM che seguono l'approccio error-first nelle funzioni di callback. In questo approccio il primo argomento della funzione di callback sarà sempre l'errore o l'oggetto errore sollevato in caso di eccezione.

const fs = require('fs');

fs.writeFile('test', 'Test', err => {
    if(err) {
        //...
    }
});

In questo caso, ad esempio, l'errore può essere sollevato se Node non ha sufficienti permessi per scrivere nel file system.

A questo punto dobbiamo introdurre un costrutto fondamentale del linguaggio JavaScript che per la verità è poco usato dagli sviluppatori, ossia try/catch/finally.

  1. try: in questo blocco il codice viene valutato e se solleva un'eccezione, il controllo viene passato al blocco catch.
  2. catch: qui abbiamo a disposizione l'oggetto errore con cui possiamo ad esempio informare l'utente circa l'avvenuto problema.
  3. finally: questo blocco viene sempre eseguito a prescindere dall'esito della valutazione.

Vediamolo in azione.

router.post('/price', (req, res, next) => {
    const { price } = req.body;
      let formattedPrice = '';
    try {
        formattedPrice = parseFloat(price).toFixed(2);
    } catch(err) {
        formattedPrice = 'N/A';
    } finally {
        formattedPrice += ' Euro';
    }
    res.json({ formattedPrice });
});

La documentazione ci mostra diversi esempi dell'uso pratico di questo costrutto. Per l'utente finale è importante ricordare che quando l'errore viene intercettato il messaggio da mostrare non dovrà mostrare i dettagli tecnici dell'errore, dettagli che andrebbero invece registrati nei log dell'applicazione.

Questo costrutto funge da introduzione al problema degli errori sollevati dalle Promise. Le Promise hanno questa particolarità: l'oggetto errore del blocco catch può essere vuoto se nel blocco try l'eccezione non è stata sollevata da una Promise con il suo callback di tipo reject ma da un'espressione JavaScript. Ad esempio:

router.post('/products/price/:id', async (req, res, next) => {
   const { id } = req.params;
   try {
       const product = await products.findById(id);
       const formattedPrice = parseFloat(product.price).toFixed(2);
       res.json({ formattedPrice });
   } catch(err) {
       res.status(500);
       res.json({ err });
   }
});

In questo caso può accadere che il prodotto non esista ma questo non solleva un'eccezione. Invece l'errore viene creato quando si cerca di formattare il prezzo. Ciò che verrà restituito dal nostro endpoint non saranno i dettagli dell'errore ma un oggetto vuoto.

Gli errori HTTP possono essere sollevati sia dai moduli core di Node sia da quei moduli NPM che gestiscono richieste HTTP. Questi errori possono essere o di inizializzazione o di elaborazione della richiesta.

Un errore tipico di inizializzazione è quando omettiamo un parametro di configurazione della richiesta. Invece un errore di elaborazione può avere luogo ad esempio quando stiamo effettuando una richiesta usando SSL/TLS e il server remoto non dispone di un certificato valido.

L'errore nei moduli core viene gestito dall'evento error della richiesta.

const https = require('https');

https.get('https://api.test/posts', resp => {

}).on('error',  err => {
  console.log(err);
});

Questa gestione asincrona ci permette di fare in modo che l'elaborazione della risposta da parte del server venga condizionata dalla presenza o meno di errori HTTP. In questo caso infatti se l'evento ha luogo verrà subito impedita l'elaborazione della risposta.

I moduli NPM, infine, richiedono uno studio attento della loro documentazione, in particolare della loro gestione degli errori verificando ad esempio se il modulo in questione sia basato sulle Promise o sull'approccio error-first nelle funzioni di callback. In base all'approccio usato si dovrà implementare una strategia diversa.

Esistono, a onor del vero, anche moduli che hanno un approccio radicale: ad esempio il modulo validator accetta solo stringhe come input e qualsiasi altro valore solleva un errore fatale.