Errori ed eccezioni, cosa fare quando le cose vanno male

Questo è il post #9 di 12 nella serie “Fondamenti di Programmazione 101

Spesso sento dire, “Scrivere software è complicato” e sapete cosa? È vero, scrivere software è complicato e scrivere buon software è ancora più complicato!

A causa di questo, è inevitabile che commetteremo errori e non importa quanto ci sforzeremo, in maniera non intenzionale, commetteremo errori.

Probabilmente lo avete già sentito da qualcuno o magari lo avrete provato su voi stessi ma non è così raro che persone spendano giorni e qualche volta anche settimane per cercare di capire dove è l’origine di un errore.

Capire cosa è un bug software

Il termine bug per descrivere un errore in un programma ha origini molto anteriori alla nascita dei computers. Abbiamo tracce nella storia che utilizzano questo termine risalenti al 1870!

In una lettera, datata 1978, Thomas Edison scrisse a un suo associato:

“It has been just so in all of my inventions. The first step is an intuition, and comes with a burst, then difficulties arise — this thing gives out and [it is] then that “Bugs” — as such, little faults and difficulties are called — show themselves and months of intense watching, study, and labor are requisite before commercial success or failure is certainly reached.”

Una storia, a cui spesso viene attribuita l’origine del termine bug in software, è attribuita a Grace Hopper.
Nel 1946, lei si unì alla facoltà di Harvard al Computation Laboratory, dove continuò il suo lavoro sui computers Mark I e Mark II.
Il computer Mark II produceva errori e dopo alcune ricerche, gli operatori trovarono che la causa era una falena intrappolata in un relay. La falena fu rimossa e inserita in un blocco note. Sotto la falena scrissero:

“First actual case of a bug being found.”

La data nel blocco note era il 9 Settembre 1947 e quello fu il primo caso in cui il termine bug fu usato in ambito dei computers:

Primo bug conosciuto in un programma software

The moth found in the Mark II computer in 1947 – US Naval Historical Center Online Library Photograph (Public Domain [PD])

NASA’s Mars Climate Orbiter

Quello che vi racconterò adesso è uno dei bug più famosi nella storia dei computers. Il Mars Climate Orbiter era una sonda spaziale che è stata lanciata dalla NASA l'11 Dicembre del 1998.
La sua missione era di studiare il clima Marziano, l’atmosfera e i cambi sulla superficie. Il 23 Settembre del 1999, tutte le comunicazioni con la sonda spaziale andarono perse. Non si sa se sia stata distrutta nell’atmosfera Marziana o se abbia continuato a esistere nello spazio.

Il 10 Novembre del 1999, una commissione investigativa ha rilasciato un report. In esso è stata fatta chiarezza che la causa per il disastro era dovuta a un bug in un pezzo di software sviluppato da un fornitore della NASA. Il bug era dovuto al fatto che il software sviluppato dal fornitore forniva un dato in una data unità di misura, mentre il software della NASA se lo aspettava in un unità di misura differente.

Il costo dell’errore è stato stimato in 125 milioni di dollari americani!

The Morris worm

Nel 1988, uno studente della Cornell University chiamato Robert Morris rilasciò un computer worm che fu progettato per essere innocuo. Sfruttando alcuni difetti nel programma sendmail del sistema operativo Unix il worm passava da computer a computer. Quando un nuovo computer veniva trovato, il programma controllava se il computer fosse stato già infettato.

Morris capì che questo sarebbe stato un modo semplice per gli amministratori di sistema per fermare la diffusione e aiutare il sistemista a identificare se fosse già stato infettato. Per compensare ciò, Morris progettò il suo worm in modo che infettasse qualsiasi computer che rispondesse con un si su sette volte.

Questo fu il grande errore che ha fatto si che il worm non solo si diffondesse rapidamente su Internet, ma infettasse anche gli stessi computers più volte.

Il worm creato da Morris è stato il primo worm che Internet ha conosciuto e il costo per ripulire i danni fatti fu stimato in circa 100 milioni di dollari americani. Morris fu multato con 10.000 dollari ma almeno ha fatto carriera successivamente.
Adesso Morris è professore al Massachusetts Institute of Technology. Un disco con il codice sorgente del worm è esposto al Computer History Museum in California.


Definiamo cosa è un bug software

Per comprendere cosa è un bug software vediamo la definizione data da [Techopedia](https://www.techopedia. com/definition/24864/software-bug-):

A software bug is a problem causing a program to crash or produce invalid output. The problem is caused by
insufficient or erroneous logic. A bug can be an error, mistake, defect, or fault, which may cause failure or
deviation from expected results.

Quindi un bug può causare un programma ad avere un comportamento non voluto e questo capita quando ci aspettiamo che un programma funzioni in una data maniera ma in realtà fa qualcosa di diverso.

Tipi di bug software

Ci sono diversi modi di classificare bugs software, analizziamo alcuni dei tipi più comuni.

ERRORI ARITMETICI

Divisione per zero

Questo tipo di errore non è specifico dei programmi software, anche in aritmetica dividere un numero per zero non darà un numero finito.

Posso già sentire qualcuno dire: “Ma io non dividerò mai un numero per zero nel mio programma, non ha senso!

Il problema nasce quando nel nostro programma abbiamo della logica che fa delle divisioni e i numeri sui quali vogliamo eseguire delle operazioni sono delle variabili. In quel caso, può capitare, che la variabile che usiamo per effettuare la divisione abbia il valore zero magari perché ottenuta da una precedente computazione che ritorna un valore non atteso, appunto lo zero.

Supponiamo di avere una variabile x il cui valore è 10 e una variabile y il cui valore è zero e vogliamo effettuare una divisione x / y:

Divisione per zero  - Esempio

Questo è un esempio in JavaScript e vediamo che il risultato della divisione è Infinity e ovviamente questo può causare ulteriori comportamenti non voluti nel caso un cui usassimo la variabile result per ulteriori logiche nel nostro programma.
È importante notare che altri linguaggi di programmazione posso lanciare un errore immediatamente anziché ritornare un valore come nel caso di JavaScript.

Quindi è importante fare i dovuti controlli prima di effettuare una divisione o gestire la possibilità che possa essere successo un problema.

Errore aritmetico di overflow/underflow

Per dare un esempio di questo tipo di errori, supponiamo di stare lavorando con un linguaggio di programmazione che usa un tipo di dato chiamato byte. Questo specifico tipo di dato può contenere valori nel range di -128 fino a 127.

Quindi possiamo creare una variabile di tipo byte assegnando un valore all’intero di quel range:

my_byte = 127

A questo punto cosa succede se incrementiamo il valore della variabile my_byte di uno?

my_byte = my_byte + 1

Ovviamente, facendo 127 + 1 ci aspettiamo il valore 128 ma con grande sorpresa vediamo che restituisce -128!

Il motivo è che quando una variabile contiene un valore numerico che è il massimo numero che possa essere rappresentato per quel dato tipo di dato, quando incrementiamo il valore di uno otteniamo il più piccolo valore che possa essere rappresentato per quel tipo di dato specifico. Questo è chiamato un errore di overflow.

In maniera speculare, se siamo al valore più piccolo che un dato tipo di dato possa rappresentare e decrementiamo di uno, otterremo il più grande valore che possa essere rappresentato per quel dato tipo di dato.
Questo è chiamato un errore di underflow.

Perdita di precisione

In programmazione, lavorare con numeri a virgola variabile è una cosa delicata e rischiamo di perdere precisione nella rappresentazione del valore quando vengono fatti arrotondamenti.

Supponiamo di avere questo snippet di codice:

a = 1.3 b = 1.1 print a + b

Ovviamente ci aspettiamo che il risultato stampato sia: 2.4

Vediamo come JavaScript rappresenta il risultato di questa operazione:

Perdita di precisione  in operazione aritmetica

Minimo per come possa sembrare l’errore, ci sono dei tipi di applicazioni (per esempio in ambito finanziario) in cui questo non è ammissibile!

ERRORI LOGICI

Errori di questo tipo solitamente non causano il crash di un programma ma causano comportamenti non voluti.

Ci sono varie possibilità per commettere questo tipo di errori, per esempio possiamo usare un operatore non corretto. Supponiamo di voler eseguire una data logica se per esempio un auto sta andato a una velocità superiore ai 120 Km/h e scriviamo la nostra condizione nel seguente modo:

Operazione con operazione non corretto

Ovviamente abbiamo usato l’operatore sbagliato < invece di >.

Un altro esempio molto comune, specialmente per un principiante, è quello di utilizzare = invece di ==. Per esempio, se volessimo allertare il guidatore che abbiamo raggiunto la velocità di 120 Km/h e scriviamo la condizione in questa maniera:

Operazione con operatore non corretto - Esempio 2

Anziché controllare l’uguaglianza, qui stiamo facendo un assegnamento causando il programma ad avere un comportamento diverso da quello voluto.

Altri errori abbastanza comuni accadono quando facciamo confusione con gli operatori booleani quindi per esempio, quando invertiamo l’uso dell’AND e dell’OR e così via.

Questi sono errori banali e spesso causati anche dalla stanchezza a cui dobbiamo stare molto attenti.

ERRORI DI SINTASSI

La regola che ci dice come dobbiamo scrivere il nostro codice, in un dato linguaggio di programmazione, è chiamata la sua sintassi. Quando scriviamo del codice che non segue le regole definite per un dato linguaggio, otteniamo un errore di sintassi.

Fortunatamente, questo tipo di errori sono molto più semplici da scovare in quanto il compilatore o l’interprete ce li segnaleranno immediatamente e ci daranno delle indicazioni su cosa ha causato l’errore e il perché.

Il debugger è nostro amico

Un debugger è un tool che ci può aiutare a vedere cosa sta accadendo nel nostro programma mentre è in esecuzione.
Spesso, quando abbiamo qualche problema, non è così facile scovarne l’origine solo eseguendo il programma.

Breakpoints

Un altro funzionalità molto importante dei debuggers è la possibilità di settare dei breakpoints il cui scopo è quello, durante l’esecuzione del programma, di fermare l’esecuzione sul punto in cui è stato settato il breakpoints. Una volta messa in pausa l’esecuzione del programma, possiamo ispezionare lo stato del programma in quel dato momento nel tempo, valutando il contenuto di variabili, cambiandone il valore, ecc..

È uno strumento veramente potente e ogni programmatore dovrebbe avere una certa dimestichezza nell’usarlo adeguatamente.

Lavorare con le eccezioni

Un eccezione è un errore o un evento inaspettato che si verifica durante l’esecuzione di un programma. È causato da una condizione nel software dove il programma ha raggiunto uno stato tale che non può più continuare la sua esecuzione.

Motivi comuni che causano le eccezioni

Ci possono essere svariate ragioni che causano il nascere di un eccezione. Se per esempio, un file richiesto dal nostro programma non è presente oppure se proviamo ad accedere a un indice di un array che è più grande della dimensione dell’ array stesso e così via.

La maggior parte dei linguaggi di programmazione supportano il concetto di eccezione e l’idea dietro il suo funzionamento è pressoché uguale.

Quando il programma lancia un eccezione, può non essere così semplice interpretare il messaggio di errore che otteniamo e questo è dovuto a quello che viene definito il call stack dell’eccezione. Cerchiamo di capire meglio di cosa si tratta.

Eccezioni e il call stack

Supponiamo, nel nostro programma, di avere una funzione principale chiamata main. Questa funzione, al suo interno, invoca un altra funzione che a sua volta invoca un altra funzione e così via. Graficamente avremo qualcosa del genere:

Esempio di call-stack di una funzione

Tenere traccia di dove ci troviamo nella catena di chiamate delle funzioni è chiamato il call stack ed è gestito dal linguaggio di programmazione durante l’esecuzione del programma.

Adesso immaginiamo di ricevere un eccezione nell’ultima funzione chiama c. L’esecuzione uscirà immediatamente dalla funzione c a causa dell’eccezione e il controllo del programma passerà alla funzione chiamante, in questo caso la funzione b. L’esecuzione uscirà immediatamente dalla funzione b per lo stesso motivo e il controllo passerà alla funzione chiamante, in questo caso la funzione a e il processo si ripeterà fino a che il controllo non passa alla funzione principale, la funzione main.

Arrivati a questo punto, anche la funzione main uscirà e visto che è la prima funzione chiamata nel nostro programma, questo causerà l’uscita completa dal programma.

La ragione per la quale ogni funzione terminerà la propria esecuzione è che nessuna di esse gestisce l’eccezione.

Gestire le eccezioni

Per capire come poter gestire un eccezione, prima abbiamo la necessità di capire quale è l’origine del problema.
Una volta individuata l’origine, possiamo intervenire introducendo delle misure di protezione.

Eccezioni posso essere “catturate” ma dobbiamo specificare quale tipo di eccezione vogliamo gestire. Se riceviamo un eccezione diversa da quella che abbiamo dichiarato, essa non verrà gestita e il programma crasherà come nell’esempio precedente.

Possiamo usare il costrutto try...catch per wrappare le chiamate di funzioni che possono rilanciare un eccezione evitando che il programmi crashi e inoltre, avendo la possibilità di eseguire delle azioni diverse per continuare l’esecuzione del programma.


Ricapitoliamo

Un bug software è un errore nel programma che può avere diverse cause. Il modo di risolvere un bug dipende dal tipo di errore che si è verificato.

In casi come un errore di sintassi, il compilatore o l’interprete ci indirizzeranno immediatamente prima ancora che il programma venga eseguito.

Un eccezione si verifica quando il programma fa qualcosa che non è supposto fare. Quando ciò accade, l’eccezione interromperà immediatamente l’esecuzione del programma se l’eccezione non viene gestita.
Fortunatamente, i linguaggi di programmazione ci mettono a disposizione degli strumenti, come il blocco try/catch, attraverso il quale è possibile gestire il caso di quando un eccezione viene rilanciata.