modulo 02 / lezione 13

Leggere errori e stack trace

Sintassi, runtime, logica: tre voci diverse

Capire perché non tutti gli errori parlano nello stesso modo e alcuni non generano eccezioni. Introdurre il concetto di assert.

45 minDebugging

In parole semplici

Le tre famiglie di errore introdotte nel modulo 0 (compile-time/sintassi, runtime, logica) hanno tre voci diverse: una grida prima ancora di partire, una grida durante l’esecuzione, la terza non grida affatto. Per gli errori silenziosi serve uno strumento dedicato: l’assertion.

Parole nuove

  • eccezione (exception): meccanismo con cui un linguaggio segnala un errore a runtime.
  • assertion (assert): controllo esplicito che fallisce se una condizione attesa non è vera.
  • invariante: condizione che deve essere sempre vera in un certo punto del programma.
  • pre/postcondizione: condizione che deve valere prima/dopo una funzione.

Cosa impari

  • Riconoscere la “voce” giusta di ogni famiglia.
  • Usare assert per dare voce agli errori logici.
  • Distinguere assertion di sviluppo (verifica) da validazione di input.

Le tre voci

Voce 1: sintassi (compile-time)

Forte e immediata, prima ancora dell’esecuzione.

function saluta(nome
{
  return "Ciao " + nome;
}
SyntaxError: Unexpected token '{'

L’editor o il parser ti fermano. Non ci sono dubbi né interpretazione.

Voce 2: runtime (eccezione)

Forte ma in differita: emerge quando il codice esegue il punto problematico.

function calcolaSconto(prezzo, percentuale) {
  return prezzo * (1 - percentuale / 100);
}
calcolaSconto(undefined, 20);
// NaN  ← attenzione: niente eccezione!

In questo caso il runtime non lancia eccezione. Restituisce NaN. Il bug è silenzioso. Andiamo a vedere la terza voce.

Voce 3: logica (silenziosa)

Nessuna eccezione, nessuna parola del runtime. Il programma “funziona”. Solo che il risultato è sbagliato.

function eta(annoNascita) {
  return 2024 - annoNascita;  // bug: anno hardcoded
}
eta(1990);  // 34 nel 2025, dovrebbe essere 35

Nessun errore. Il bug si scopre solo confrontando l’output con quello atteso.

Dare voce alla logica: le assertion

Un assert è una riga che dichiara: “qui questa condizione deve essere vera; se non lo è, ferma tutto e dimmelo”.

In JavaScript / Node

import { strict as assert } from "node:assert";

function eta(annoNascita, annoCorrente) {
  assert(typeof annoNascita === "number", "annoNascita deve essere numero");
  assert(annoNascita > 1900, "annoNascita troppo basso");
  assert(annoNascita <= annoCorrente, "annoNascita nel futuro");
  return annoCorrente - annoNascita;
}

eta(undefined, 2025);
// AssertionError: annoNascita deve essere numero

In browser puoi usare console.assert:

console.assert(typeof annoNascita === "number", "annoNascita deve essere numero");

console.assert non interrompe l’esecuzione, ma scrive in console quando fallisce. Per fermare davvero serve throw o assert nativo.

In Python

def eta(anno_nascita, anno_corrente):
    assert isinstance(anno_nascita, int), "anno_nascita deve essere int"
    assert anno_nascita > 1900
    assert anno_nascita <= anno_corrente
    return anno_corrente - anno_nascita

In Java / Swift / Go

assert annoNascita > 1900 : "annoNascita troppo basso";
assert(annoNascita > 1900, "annoNascita troppo basso")
precondition(annoCorrente >= annoNascita)
if annoNascita <= 1900 {
    panic("annoNascita troppo basso")
}

Assertion ≠ validazione di input

Distinzione importante:

  • Validazione input: controlli su dati che vengono dall’esterno (utente, API, file). Fallimento = errore “atteso”, da gestire con messaggi e fallback.
  • Assertion: controlli su invarianti del tuo codice. Fallimento = bug, da correggere.

Esempio:

function aggiungiAlCarrello(carrello, prodotto) {
  // validazione input: il chiamante potrebbe sbagliare
  if (!prodotto || prodotto.prezzo < 0) {
    throw new Error("Prodotto non valido");
  }

  // assertion: la mia logica garantisce che carrello sia un array
  assert(Array.isArray(carrello), "carrello deve essere array");

  carrello.push(prodotto);

  // postcondizione
  assert(carrello.length > 0);
  return carrello;
}

Quando piazzare assertion

  • All’ingresso di una funzione: precondizioni.
  • All’uscita: postcondizioni.
  • Nei punti dove credi “non potrebbe mai succedere”: è esattamente lì che succedono i bug.
  • Quando trovi un bug logico: aggiungi un assert che lo avrebbe colto prima.

Esercizio guidato

Riprendi una funzione che hai scritto in passato. Per ognuna:

  1. Aggiungi una precondizione su ogni parametro.
  2. Aggiungi una postcondizione sul valore di ritorno.
  3. Aggiungi una assertion in un punto interno dove “non dovrebbe mai succedere”.
  4. Esegui con input volutamente sbagliati e verifica quali assertion scattano.

Errori frequenti del principiante

  • Usare assert al posto della validazione input. Il chiamante può essere “sporco”; l’assert è per te.
  • Disattivare assertion in produzione. Alcuni linguaggi lo fanno di default (Python con -O, Java con flag). Sappi cosa stai disattivando.
  • Lasciare assertion vaghe. assert(x) senza messaggio non aiuta. assert(x, "x deve essere > 0 dopo normalizzazione") sì.
  • Pensare che assertion rallentino. In dev sono trascurabili; in produzione si possono disabilitare se serve.