Gestione delle Eccezioni in C#

Impara a gestire gli errori in modo professionale con try-catch-finally e creare codice robusto

🎯 Cosa Sono le Eccezioni?

Un’eccezione è un errore che si verifica durante l’esecuzione di un programma.
Quando si verifica un errore, il programma “lancia” (throw) un’eccezione. Se non viene gestita,
il programma si interrompe bruscamente (crash).

💡 Perché Gestire le Eccezioni?

Immagina un’applicazione bancaria: se l’utente inserisce testo invece di un numero,
l’app non deve crashare! Deve mostrare un messaggio chiaro e permettere di riprovare.
La gestione delle eccezioni rende il tuo software robusto e
user-friendly.

Esempio: Cosa Succede SENZA Gestione Eccezioni
class Program 
{
    static void Main() 
    {
        Console.Write("Inserisci un numero: ");
        string input = Console.ReadLine();
        
        // ❌ SE l'utente scrive "abc" invece di un numero...
        int numero = int.Parse(input);  // CRASH! FormatException
        
        Console.WriteLine($"Il doppio è: {numero * 2}");
        // Questa riga NON viene mai eseguita se c'è errore!
    }
}

// Output se input = "abc":
// Unhandled exception. System.FormatException: Input string was not in a correct format.
// PROGRAMMA TERMINATO BRUSCAMENTE

Esempio: CON Gestione Eccezioni
class Program 
{
    static void Main() 
    {
        Console.Write("Inserisci un numero: ");
        string input = Console.ReadLine();
        
        try 
        {
            // Codice che potrebbe generare un'eccezione
            int numero = int.Parse(input);
            Console.WriteLine($"Il doppio è: {numero * 2}");
        }
        catch (FormatException) 
        {
            // Gestione dell'errore
            Console.WriteLine("❌ Errore: Devi inserire un numero valido!");
        }
        
        Console.WriteLine("Programma terminato normalmente.");
    }
}

// Output se input = "abc":
// ❌ Errore: Devi inserire un numero valido!
// Programma terminato normalmente.
// ✅ NESSUN CRASH!

✨ Vantaggi della Gestione Eccezioni

Robustezza

Il programma non crasha per errori comuni

User Experience

Messaggi chiari invece di crash misteriosi

Debugging

Informazioni dettagliate sull’errore

Professionalità

Software di qualità enterprise

🛡️ Try-Catch-Finally

La struttura try-catch-finally è il meccanismo principale per gestire le eccezioni in C#.

1
Try-Catch Base

Il blocco try contiene il codice rischioso, catch gestisce l’errore.

Sintassi Base
try 
{
    // Codice che potrebbe generare un'eccezione
    int[] numeri = { 1, 2, 3 };
    Console.WriteLine(numeri[10]);  // IndexOutOfRangeException!
}
catch (Exception ex) 
{
    // Gestione dell'errore
    Console.WriteLine($"Si è verificato un errore: {ex.Message}");
}

// Il programma continua qui...

2
Catch Multipli

Gestisci diversi tipi di eccezioni in modo specifico.

Catch Multipli (dal più specifico al più generico)
try 
{
    Console.Write("Inserisci numero: ");
    string input = Console.ReadLine();
    int numero = int.Parse(input);
    
    Console.Write("Dividi per: ");
    int divisore = int.Parse(Console.ReadLine());
    
    int risultato = numero / divisore;
    Console.WriteLine($"Risultato: {risultato}");
}
catch (FormatException) 
{
    Console.WriteLine("❌ Devi inserire un numero valido!");
}
catch (DivideByZeroException) 
{
    Console.WriteLine("❌ Non puoi dividere per zero!");
}
catch (OverflowException) 
{
    Console.WriteLine("❌ Il numero è troppo grande!");
}
catch (Exception ex)  // Catch generico sempre per ultimo!
{
    Console.WriteLine($"❌ Errore imprevisto: {ex.Message}");
}

3
Finally – Sempre Eseguito

Il blocco finally viene SEMPRE eseguito, anche se c’è un’eccezione.

Try-Catch-Finally
FileStream file = null;

try 
{
    file = new FileStream("dati.txt", FileMode.Open);
    // Leggi file...
    Console.WriteLine("File letto con successo");
}
catch (FileNotFoundException) 
{
    Console.WriteLine("❌ File non trovato!");
}
catch (IOException ex) 
{
    Console.WriteLine($"❌ Errore I/O: {ex.Message}");
}
finally 
{
    // Questo blocco viene SEMPRE eseguito
    // Perfetto per chiudere risorse (file, connessioni, ecc.)
    if (file != null) 
    {
        file.Close();
        Console.WriteLine("File chiuso correttamente");
    }
}

Console.WriteLine("Programma terminato");

📌 Ordine di Esecuzione Try-Catch-Finally

try 
{
    Console.WriteLine("1. Eseguo try");
    throw new Exception("Errore!");
    Console.WriteLine("Questa riga NON viene eseguita");
}
catch (Exception) 
{
    Console.WriteLine("2. Eseguo catch (c'è stata un'eccezione)");
}
finally 
{
    Console.WriteLine("3. Eseguo finally (SEMPRE eseguito)");
}

Console.WriteLine("4. Continuo con il resto del programma");

// Output:
// 1. Eseguo try
// 2. Eseguo catch (c'è stata un'eccezione)
// 3. Eseguo finally (SEMPRE eseguito)
// 4. Continuo con il resto del programma

💡 Quando Usare Finally

Finally è essenziale per liberare risorse:

  • Chiudere file aperti
  • Chiudere connessioni database
  • Liberare memoria allocata
  • Rilasciare lock e semafori

Alternativa moderna: Usa using statement per gestione automatica
(lo vedremo dopo).

⚠️ Tipi Comuni di Eccezioni

C# fornisce molti tipi di eccezioni predefinite per situazioni diverse.

Eccezione Quando Si Verifica Esempio
FormatException Input in formato errato int.Parse("abc")
DivideByZeroException Divisione per zero 10 / 0
IndexOutOfRangeException Indice array fuori range array[100] (su array piccolo)
NullReferenceException Accesso a oggetto null string s = null; s.Length
ArgumentNullException Parametro null non consentito Metodo riceve null quando non deve
ArgumentException Argomento non valido Parametro fuori range ammissibile
FileNotFoundException File non esistente Apertura file inesistente
IOException Errori input/output Problemi lettura/scrittura file
InvalidOperationException Operazione non valida Chiamata metodo in stato non valido
OverflowException Numero troppo grande/piccolo Risultato supera limiti tipo
Esempi Pratici Eccezioni Comuni
// ===== FormatException =====
try 
{
    int numero = int.Parse("abc123");  // ❌ Formato non valido
}
catch (FormatException) 
{
    Console.WriteLine("Formato numero non valido");
}

// ===== NullReferenceException =====
try 
{
    string testo = null;
    int lunghezza = testo.Length;  // ❌ testo è null!
}
catch (NullReferenceException) 
{
    Console.WriteLine("Tentativo di usare oggetto null");
}

// ===== IndexOutOfRangeException =====
try 
{
    int[] array = { 1, 2, 3 };
    Console.WriteLine(array[10]);  // ❌ Indice non esiste
}
catch (IndexOutOfRangeException) 
{
    Console.WriteLine("Indice array fuori range");
}

// ===== ArgumentNullException =====
public void ProcessaStringa(string testo) 
{
    if (testo == null) 
    {
        throw new ArgumentNullException(nameof(testo), "Il testo non può essere null");
    }
    // ... elaborazione
}

🚀 Lanciare Eccezioni (throw)

Puoi lanciare (throw) eccezioni manualmente per segnalare errori nel tuo codice.

1
Throw Base

Usa throw per lanciare un’eccezione.

Throw Exception
public void ImpostaEtà(int età) 
{
    if (età < 0) 
    {
        throw new ArgumentException("L'età non può essere negativa!", nameof(età));
    }
    
    if (età > 150) 
    {
        throw new ArgumentException("L'età non è realistica!", nameof(età));
    }
    
    this.Età = età;
}

// Utilizzo
try 
{
    persona.ImpostaEtà(-5);  // Lancia ArgumentException
}
catch (ArgumentException ex) 
{
    Console.WriteLine($"Errore: {ex.Message}");
    // Output: Errore: L'età non può essere negativa!
}

2
Re-throw

Puoi catturare un’eccezione, fare qualcosa, e ri-lanciarla.

Re-throw Exception
public void LeggiFile(string percorso) 
{
    try 
    {
        string contenuto = File.ReadAllText(percorso);
        // ... elaborazione
    }
    catch (FileNotFoundException ex) 
    {
        // Log dell'errore
        Console.WriteLine($"[LOG] File non trovato: {percorso}");
        
        // Re-lancia l'eccezione per farla gestire a chi chiama
        throw;  // ← Ri-lancia la stessa eccezione
    }
}

// Chiamante
try 
{
    LeggiFile("config.txt");
}
catch (FileNotFoundException) 
{
    Console.WriteLine("Configurazione non trovata, uso default");
}

3
Eccezioni Custom

Crea le tue eccezioni personalizzate per situazioni specifiche.

Eccezione Personalizzata
// Definizione eccezione custom
public class SaldoInsufficienteException : Exception 
{
    public decimal SaldoDisponibile { get; set; }
    public decimal ImportoRichiesto { get; set; }
    
    public SaldoInsufficienteException(decimal saldo, decimal importo) 
        : base($"Saldo insufficiente. Disponibile: €{saldo}, Richiesto: €{importo}") 
    {
        SaldoDisponibile = saldo;
        ImportoRichiesto = importo;
    }
}

// Utilizzo
public class ContoBancario 
{
    public decimal Saldo { get; private set; }
    
    public void Preleva(decimal importo) 
    {
        if (importo > Saldo) 
        {
            throw new SaldoInsufficienteException(Saldo, importo);
        }
        
        Saldo -= importo;
    }
}

// Test
try 
{
    ContoBancario conto = new ContoBancario { Saldo = 100 };
    conto.Preleva(150);
}
catch (SaldoInsufficienteException ex) 
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Disponibile: €{ex.SaldoDisponibile}");
    Console.WriteLine($"Richiesto: €{ex.ImportoRichiesto}");
}

🔧 Using Statement

Il using statement gestisce automaticamente la pulizia delle risorse,
garantendo che Dispose() venga chiamato anche in caso di eccezioni.

Using Statement – Gestione Automatica Risorse
// ❌ SENZA using (manuale, rischioso)
StreamReader reader = null;
try 
{
    reader = new StreamReader("file.txt");
    string contenuto = reader.ReadToEnd();
    Console.WriteLine(contenuto);
}
catch (IOException ex) 
{
    Console.WriteLine($"Errore: {ex.Message}");
}
finally 
{
    if (reader != null) 
    {
        reader.Dispose();  // Dobbiamo ricordarci!
    }
}

// ✅ CON using (automatico, sicuro)
try 
{
    using (StreamReader reader = new StreamReader("file.txt")) 
    {
        string contenuto = reader.ReadToEnd();
        Console.WriteLine(contenuto);
        
        // reader.Dispose() viene chiamato AUTOMATICAMENTE
        // anche se si verifica un'eccezione!
    }
}
catch (IOException ex) 
{
    Console.WriteLine($"Errore: {ex.Message}");
}

// ✅ Using moderno (C# 8.0+)
try 
{
    using StreamReader reader = new StreamReader("file.txt");
    string contenuto = reader.ReadToEnd();
    Console.WriteLine(contenuto);
    
    // Dispose chiamato automaticamente a fine scope
}
catch (IOException ex) 
{
    Console.WriteLine($"Errore: {ex.Message}");
}

💡 IDisposable

using funziona con oggetti che implementano IDisposable.
Comuni esempi:

  • StreamReader / StreamWriter (file)
  • SqlConnection / SqlCommand (database)
  • HttpClient (HTTP requests)
  • Graphics (disegno)

✅ Best Practices

1
Cattura Eccezioni Specifiche

Evita catch (Exception) generico quando possibile.

Best Practice: Specifico → Generico
// ❌ MALE: Troppo generico
try 
{
    int numero = int.Parse(input);
}
catch (Exception) 
{
    Console.WriteLine("Errore generico");  // Poco utile!
}

// ✅ BENE: Specifico
try 
{
    int numero = int.Parse(input);
}
catch (FormatException) 
{
    Console.WriteLine("Il valore inserito non è un numero valido");
}
catch (OverflowException) 
{
    Console.WriteLine("Il numero è troppo grande");
}
catch (ArgumentNullException) 
{
    Console.WriteLine("Nessun valore inserito");
}

2
Non Usare Eccezioni per Controllo Flusso

Le eccezioni sono costose. Usa validazione normale quando possibile.

Anti-pattern vs Best Practice
// ❌ MALE: Usa eccezioni per controllo flusso
try 
{
    int numero = int.Parse(input);
}
catch (FormatException) 
{
    numero = 0;  // Default
}

// ✅ BENE: Usa TryParse
int numero;
if (!int.TryParse(input, out numero)) 
{
    numero = 0;  // Default
}

// Più veloce, più chiaro, nessuna eccezione!

3
Messaggi di Errore Chiari

Aiuta gli utenti a capire cosa è andato storto e come risolvere.

Messaggi Utili
// ❌ MALE
catch (Exception ex) 
{
    Console.WriteLine("Errore");
}

// ✅ BENE
catch (FileNotFoundException ex) 
{
    Console.WriteLine($"❌ File non trovato: {ex.FileName}");
    Console.WriteLine("Assicurati che il file esista nella directory corretta.");
}
catch (UnauthorizedAccessException) 
{
    Console.WriteLine("❌ Permessi insufficienti per accedere al file.");
    Console.WriteLine("Esegui il programma come amministratore.");
}

4
Log delle Eccezioni

Registra le eccezioni per debugging e monitoraggio.

Logging Eccezioni
public void ProcessaDati(string percorso) 
{
    try 
    {
        // Elaborazione...
    }
    catch (Exception ex) 
    {
        // Log completo per debugging
        LogErrore(ex);
        
        // Messaggio user-friendly
        Console.WriteLine("Si è verificato un errore durante l'elaborazione.");
        
        // Re-throw se necessario
        throw;
    }
}

private void LogErrore(Exception ex) 
{
    string log = $"[{DateTime.Now}] ERRORE: {ex.GetType().Name}\n";
    log += $"Messaggio: {ex.Message}\n";
    log += $"Stack Trace: {ex.StackTrace}\n";
    
    File.AppendAllText("errori.log", log);
}

🎯 Riepilogo Best Practices

  • ✅ Cattura eccezioni specifiche, non generiche
  • ✅ Usa TryParse invece di Parse + try-catch
  • ✅ Usa using per gestione automatica risorse
  • ✅ Fornisci messaggi di errore chiari all’utente
  • Log le eccezioni per debugging
  • ❌ Non catturare eccezioni che non puoi gestire
  • ❌ Non usare catch vuoti (catch { })
  • ❌ Non usare eccezioni per controllo flusso normale

🚀 Esempio Completo: Calcolatrice Robusta

Program.cs – Calcolatrice con Gestione Eccezioni
using System;

class Calcolatrice 
{
    public double Dividi(double a, double b) 
    {
        if (b == 0) 
        {
            throw new DivideByZeroException("Impossibile dividere per zero");
        }
        
        return a / b;
    }
    
    public double Radice(double numero) 
    {
        if (numero < 0) 
        {
            throw new ArgumentException("Non si può calcolare la radice di un numero negativo");
        }
        
        return Math.Sqrt(numero);
    }
    
    public double ParseNumero(string input) 
    {
        if (string.IsNullOrWhiteSpace(input)) 
        {
            throw new ArgumentNullException(nameof(input), "L'input non può essere vuoto");
        }
        
        if (!double.TryParse(input, out double risultato)) 
        {
            throw new FormatException($"'{input}' non è un numero valido");
        }
        
        return risultato;
    }
}

class Program 
{
    static void Main() 
    {
        Calcolatrice calc = new Calcolatrice();
        bool continua = true;
        
        Console.WriteLine("=== CALCOLATRICE ROBUSTA ===\n");
        
        while (continua) 
        {
            try 
            {
                // Menu
                Console.WriteLine("\nOperazioni:");
                Console.WriteLine("1. Divisione");
                Console.WriteLine("2. Radice Quadrata");
                Console.WriteLine("3. Esci");
                Console.Write("\nScelta: ");
                
                string scelta = Console.ReadLine();
                
                switch (scelta) 
                {
                    case "1":
                        EseguiDivisione(calc);
                        break;
                    
                    case "2":
                        EseguiRadice(calc);
                        break;
                    
                    case "3":
                        continua = false;
                        Console.WriteLine("Arrivederci!");
                        break;
                    
                    default:
                        Console.WriteLine("❌ Scelta non valida!");
                        break;
                }
            }
            catch (Exception ex) 
            {
                // Catch globale per errori imprevisti
                Console.WriteLine($"\n❌ ERRORE IMPREVISTO: {ex.Message}");
                LogErrore(ex);
            }
        }
    }
    
    static void EseguiDivisione(Calcolatrice calc) 
    {
        try 
        {
            Console.Write("Dividendo: ");
            double a = calc.ParseNumero(Console.ReadLine());
            
            Console.Write("Divisore: ");
            double b = calc.ParseNumero(Console.ReadLine());
            
            double risultato = calc.Dividi(a, b);
            Console.WriteLine($"\n✅ Risultato: {risultato:F2}");
        }
        catch (ArgumentNullException ex) 
        {
            Console.WriteLine($"❌ {ex.ParamName}: {ex.Message}");
        }
        catch (FormatException ex) 
        {
            Console.WriteLine($"❌ Formato non valido: {ex.Message}");
        }
        catch (DivideByZeroException ex) 
        {
            Console.WriteLine($"❌ {ex.Message}");
            Console.WriteLine("💡 Suggerimento: Inserisci un divisore diverso da zero");
        }
    }
    
    static void EseguiRadice(Calcolatrice calc) 
    {
        try 
        {
            Console.Write("Numero: ");
            double numero = calc.ParseNumero(Console.ReadLine());
            
            double risultato = calc.Radice(numero);
            Console.WriteLine($"\n✅ Radice quadrata: {risultato:F2}");
        }
        catch (ArgumentNullException ex) 
        {
            Console.WriteLine($"❌ {ex.ParamName}: {ex.Message}");
        }
        catch (FormatException ex) 
        {
            Console.WriteLine($"❌ Formato non valido: {ex.Message}");
        }
        catch (ArgumentException ex) 
        {
            Console.WriteLine($"❌ {ex.Message}");
            Console.WriteLine("💡 Suggerimento: Inserisci un numero positivo");
        }
    }
    
    static void LogErrore(Exception ex) 
    {
        string log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex.GetType().Name}: {ex.Message}\n";
        
        try 
        {
            using (StreamWriter writer = File.AppendText("errori.log")) 
            {
                writer.WriteLine(log);
            }
        }
        catch 
        {
            // Se anche il logging fallisce, non fare nulla
            // (evita loop infiniti di errori)
        }
    }
}

💡 Analisi Gestione Eccezioni
  • Try-catch stratificato: Catch specifici per ogni operazione
  • Validazione preventiva: TryParse invece di Parse
  • Throw custom: Eccezioni con messaggi chiari
  • Using statement: Gestione automatica file log
  • Messaggi user-friendly: Spiegazioni + suggerimenti
  • Logging: Traccia errori per debugging
  • Graceful degradation: Il programma non crasha mai

🎯 Riepilogo Lezione

Try-Catch-Finally:

  • try: codice rischioso
  • catch: gestione errore
  • finally: sempre eseguito
  • Catch multipli specifici
Tipi Eccezioni:

  • FormatException
  • NullReferenceException
  • ArgumentException
  • Eccezioni custom
Throw:

  • Lancia eccezioni
  • Re-throw
  • Eccezioni personalizzate
  • Validazione parametri
Best Practices:

  • Catch specifiche
  • Using statement
  • Messaggi chiari
  • Logging errori

Lezione 15 del Corso C# Completo

Torna in alto