W3docs

Best practice per la gestione delle eccezioni in Java

Regole pratiche per la gestione delle eccezioni in Java: fail fast, lanciare il tipo giusto, non ingoiare eccezioni e registrare in modo utile.

I capitoli precedenti hanno trattato la meccanica — try, catch, finally, throw, throws, classi personalizzate. Questo è il lato del giudizio. Due programmi possono usare gli stessi costrutti e uno è robusto mentre l'altro è fragile. La differenza è un piccolo insieme di abitudini che con la pratica diventano riflessi.

Fallire velocemente con input non validi

Se un metodo viene chiamato con argomenti che non può gestire, lancia immediatamente:

public void send(String to, String body) {
  if (to == null || to.isBlank()) {
    throw new IllegalArgumentException("to must be non-blank");
  }
  if (body == null) {
    throw new NullPointerException("body");
  }
  // ...real work
}

Non cercare di "fare del tuo meglio" con null. Il bug è nel chiamante e più l'eccezione è vicina a esso, più è facile da correggere. Objects.requireNonNull(body, "body") è il one-liner standard per il caso null.

L'abitudine opposta — sostituire silenziosamente i valori predefiniti agli input mancanti — porta a errori che emergono cinque livelli più in alto senza alcun indizio su chi ha passato cosa.

Lanciare il tipo più specifico che si adatta

Exception è raramente la cosa giusta da lanciare, e RuntimeException solo quando non esiste un tipo più specifico. La libreria standard ti offre un vocabolario — usalo:

  • Argomento errato → IllegalArgumentException
  • Argomento null → NullPointerException (Objects.requireNonNull)
  • Stato errato → IllegalStateException
  • Operazione non supportata → UnsupportedOperationException
  • Indice fuori intervallo → IndexOutOfBoundsException
  • Numero fuori intervallo → ArithmeticException o IllegalArgumentException

Per i fallimenti di dominio, scrivi una eccezione personalizzata invece di riutilizzare una built-in.

Catturare il tipo più specifico per cui si ha un piano

La regola simmetrica. catch (Exception e) è un segnale d'allarme. Cattura bug di programmazione (NullPointerException, IllegalStateException) e fallimenti recuperabili (IOException) ed eccezioni di librerie sconosciute tutti in un unico contenitore — e quasi sempre li gestisce in modo identico, il che è quasi sempre sbagliato.

// Bad — what does this even handle?
try { complex(); }
catch (Exception e) { log("failed"); }

// Better — specific cases get specific responses
try { complex(); }
catch (IOException e)         { retryLater(); }
catch (ParseException e)      { recordCorruptInput(e); }

Quando genuinamente non sai cosa fare con una classe di eccezione, la risposta è non catturarla. Lascia che si propaghi a un gestore che lo fa.

Non ingoiare mai un'eccezione silenziosamente

Il peggiore pattern di gestione delle eccezioni:

try { doWork(); }
catch (Exception e) { }    // never write this

Quando arriva l'inevitabile bug in produzione, non c'è stack trace, nessun messaggio, nessuna voce di log — il fallimento è semplicemente scomparso. Se intendi davvero ignorare un fallimento (raro, ma possibile — ad es. chiudere una risorsa su un percorso di cleanup), dillo esplicitamente:

try { connection.close(); }
catch (IOException ignored) {
  // close-time failure on a cleanup path; original cause already propagating
}

Il nome della variabile ignored e il commento rendono l'intento visibile al prossimo lettore.

Registrare in modo utile, registrare una volta sola

Due errori di logging sono comuni:

  • Registrare senza abbastanza contestolog.error("failed") non dice nulla.
  • Registrare e poi rilanciare — ogni livello registra la stessa eccezione e la stessa traccia finisce nel log cinque volte.

Scegli un livello che conosce il maggior contesto (solitamente ad alto livello: request handler, job runner) e registra lì con l'input che ha innescato il fallimento. I livelli sotto di esso dovrebbero concentrarsi sulla traduzione dell'eccezione, non sulla sua registrazione.

try {
  userService.activate(id);
} catch (UserNotFoundException e) {
  log.warn("activation failed: no user with id={}", id, e);   // include the exception object as the last arg
  return Response.notFound();
}

Passare l'eccezione come ultimo argomento del logger è la convenzione SLF4J — garantisce che lo stack trace completo e qualsiasi catena di cause finiscano nell'output.

Preservare la causa quando si fa wrapping

Quando traduci un'eccezione su un livello superiore, passa sempre l'originale come causa:

// Good — cause is preserved
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path, e);
}

// Bad — original IOException is lost
catch (IOException e) {
  throw new ConfigLoadException("failed to load " + path);
}

La catena Caused by: nello stack trace risultante è ciò che permette all'ingegnere di turno di risalire da un'eccezione di dominio al fallimento a livello di byte. Perderla trasforma una sessione di debug di mezz'ora in mezza giornata.

Non usare le eccezioni per il controllo del flusso

Lanciare è costoso — costruire uno stack trace alla costruzione è il costo maggiore. Ancora più importante, offusca l'intento. Un ciclo che usa try/catch (NoSuchElementException) per sapere quando fermarsi sta nascondendo ciò che sta facendo:

// Bad
try {
  while (true) {
    process(iter.next());
  }
} catch (NoSuchElementException end) { }

// Good
while (iter.hasNext()) {
  process(iter.next());
}

Quando "non trovato" è un risultato ordinario, restituisci Optional<T> o un boolean. Riserva le eccezioni per ciò che è veramente eccezionale.

Usare finally e try-with-resources per il cleanup

finally dovrebbe rilasciare risorse. try-with-resources dovrebbe essere il default per tutto ciò che è AutoCloseable. Non mettere logica di business in finally — viene eseguita sia nei percorsi di successo che di fallimento e non può distinguerli. E non usare return da finally — scarta silenziosamente l'eccezione originale o il valore di ritorno, il che è uno dei bug più difficili da diagnosticare.

Documentare ciò che si lancia

Se un metodo potrebbe lanciare un'eccezione che interessa ai chiamanti — checked o unchecked — indicalo nel Javadoc:

/**
 * Looks up a user by id.
 *
 * @throws UserNotFoundException if no user with that id exists
 * @throws IllegalArgumentException if id is null or blank
 */
public User lookup(String id) { ... }

Il compilatore impone questo per le eccezioni checked nella firma. Per quelle unchecked, il Javadoc è l'unico contratto — e i chiamanti ne hanno davvero bisogno quando l'eccezione influisce su come dovrebbero usare il metodo.

Usare le eccezioni per i fallimenti, i valori di ritorno per i risultati ordinari

La regola riassuntiva, quella che lega insieme tutto il resto. Un'eccezione dice qualcosa è andato storto e non posso rimediare qui. Un valore di ritorno dice ecco il risultato. Quando "non trovato" fa parte del normale funzionamento, restituisci Optional.empty(), un boolean o un valore sentinella. Quando "la connessione al database è caduta" avviene, lancia.

Il codice che rispetta questa distinzione è calmo: il percorso felice sembra una linea retta, il percorso insolito è in un blocco diverso e il lettore può capire a colpo d'occhio quale è quale.

Un esempio pratico

Una piccola funzione di elaborazione ordini che raccoglie le pratiche di questo capitolo — validazione fail-fast, tipi di eccezione built-in specifici, wrapping con causa e un singolo handler di alto livello che registra una volta sola.

java— editable, runs on the server

Quattro chiamate, quattro percorsi diversi. Quella riuscita ritorna normalmente. I due casi IllegalArgumentException (id null, importo zero) vengono segnalati con il messaggio che spiega cosa c'era di sbagliato nell'input. Il fallimento simulato del servizio emerge come una OrderProcessingException di dominio con l'IllegalStateException originale collegata tramite getCause(). Nulla viene ingoiato, nulla viene registrato due volte e ogni fallimento indica esattamente quale valore lo ha causato.

Cosa c'è dopo

Questo chiude la Parte 8 — hai una padronanza funzionante della macchina delle eccezioni di Java e il giudizio per usarla bene. La parte successiva approfondisce le stringhe — il tipo più comune nel codice Java di gran lunga e uno con più profondità di quanto la superficie suggerisca. Continua con Classe String in Java.

Pratica

Pratica
Quale di queste abitudini di gestione delle eccezioni è **più** probabile che renda doloroso il debug in produzione?
Quale di queste abitudini di gestione delle eccezioni è **più** probabile che renda doloroso il debug in produzione?
Was this page helpful?