Ogni programma reale incontra errori: file che non esistono, connessioni che cadono, dati in un formato inatteso, utenti che inseriscono ciò che non dovrebbero. La differenza tra un software fragile e uno robusto non sta nell'assenza di errori, ma nel modo in cui vengono gestiti. Una buona gestione degli errori rende i problemi visibili, comprensibili e circoscritti; una cattiva li nasconde finché esplodono nel momento peggiore.
Due famiglie di errori
Conviene distinguere fin dall'inizio due categorie:
- errori attesi: fanno parte del normale funzionamento. Un utente inserisce una data non valida, un file di configurazione manca, un servizio esterno risponde lentamente. Vanno previsti e gestiti con un comportamento sensato;
- errori di programmazione: sono bug. Una variabile vale
Nonedove non dovrebbe, un indice va oltre la fine di una lista. Non vanno "gestiti" nascondendoli, ma resi evidenti per poterli correggere.
Molti problemi nascono dal trattare la seconda famiglia come la prima: catturare tutto e andare avanti come se niente fosse.
L'errore più comune: il catch-all silenzioso
Questo frammento è tanto diffuso quanto dannoso:
try:
elabora_pagamento(ordine)
except Exception:
pass
Qualunque cosa vada storta, che sia un problema di rete o un refuso nel nome di una variabile, viene ignorata. Il programma continua, l'ordine risulta forse pagato e forse no, e nessun log lo racconta. Quando qualcuno se ne accorge, giorni dopo, ricostruire l'accaduto è quasi impossibile.
La regola pratica: cattura solo le eccezioni che sai gestire, nel punto in cui sai gestirle.
try:
risposta = client.invia(pagamento, timeout=10)
except TimeoutError:
logger.warning("Timeout del servizio di pagamento, ordine %s", ordine.id)
metti_in_coda_per_nuovo_tentativo(ordine)
Qui si gestisce un caso preciso, con un'azione concreta e una traccia nei log. Tutte le altre eccezioni continuano a salire.
Dove gestire un'eccezione
Non è necessario gestire un errore nel punto in cui si verifica. Spesso è meglio lasciarlo salire fino a un livello che ha il contesto per decidere. Una funzione che legge un file non sa se l'assenza del file sia grave o normale; il codice che la chiama sì.
Uno schema efficace in molte applicazioni:
- in profondità, le funzioni sollevano eccezioni chiare e specifiche;
- ai livelli intermedi, si catturano solo i casi che si possono risolvere davvero (un nuovo tentativo, un valore predefinito);
- al livello più alto (il gestore delle richieste web, il ciclo principale di un servizio), un gestore generale registra l'errore con il traceback completo e restituisce una risposta di errore pulita all'utente.
In questo modo il catch-all esiste, ma in un solo punto, e non è mai silenzioso.
Eccezioni personalizzate
Per gli errori del proprio dominio conviene definire eccezioni dedicate:
class ErroreOrdine(Exception):
"""Errore generico nella gestione degli ordini."""
class ScorteInsufficienti(ErroreOrdine):
def __init__(self, prodotto, richiesti, disponibili):
super().__init__(
f"{prodotto}: richiesti {richiesti}, disponibili {disponibili}"
)
self.prodotto = prodotto
I vantaggi sono concreti: chi chiama può catturare esattamente ScorteInsufficienti senza intercettare per errore altri problemi, il messaggio è informativo e gli attributi permettono di reagire in modo mirato, per esempio mostrando all'utente la quantità disponibile.
Conservare la causa
Quando si trasforma un'eccezione tecnica in una di dominio, è importante non perdere l'originale:
try:
dati = json.loads(testo)
except json.JSONDecodeError as e:
raise ConfigurazioneNonValida("File di configurazione illeggibile") from e
La sintassi from e collega le due eccezioni: nel traceback compariranno entrambe, con la causa tecnica precisa e il significato per l'applicazione.
Pulire le risorse
File, connessioni e lock devono essere rilasciati anche quando qualcosa va storto. Il blocco finally viene eseguito sempre, ma in Python lo strumento più pulito è il gestore di contesto:
with open("report.csv", "w") as f:
scrivi_report(f)
Il file viene chiuso in ogni caso, che scrivi_report termini normalmente o sollevi un'eccezione.
Confronto tra approcci
| Approccio | Quando ha senso |
|---|---|
| Lasciare salire l'eccezione | Il codice corrente non sa come rimediare |
| Catturare e riprovare | Errori temporanei come timeout di rete, con un limite di tentativi |
| Catturare e usare un valore predefinito | Dato facoltativo la cui assenza non compromette il risultato |
| Catturare e trasformare | Tradurre un errore tecnico in un errore di dominio con from |
| Gestore generale ai livelli alti | Registrare tutto e rispondere in modo pulito all'utente |
In sintesi
- Distingui gli errori attesi, da gestire, dagli errori di programmazione, da rendere visibili.
- Non usare mai un catch-all silenzioso: cattura solo ciò che sai gestire.
- Gestisci l'errore al livello che ha il contesto per decidere, con un unico gestore generale in cima.
- Definisci eccezioni di dominio e conserva la causa originale con
raise ... from. - Usa gestori di contesto per rilasciare sempre file, connessioni e lock.