Argomenti contro la soppressione degli errori


35

Ho trovato un pezzo di codice come questo in uno dei nostri progetti:

    SomeClass QueryServer(string args)
    {
        try
        {
            return SomeClass.Parse(_server.Query(args));
        }
        catch (Exception)
        {
            return null;
        }
    }

Per quanto ho capito, sopprimere errori come questo è una cattiva pratica, in quanto distrugge informazioni utili dall'eccezione del server originale e fa continuare il codice quando dovrebbe effettivamente terminare.

Quando è opportuno eliminare completamente tutti gli errori come questo?


13
Eric Lippert ha scritto un bel post sul blog chiamato eccezioni fastidiose in cui classifica le eccezioni in 4 diverse categorie e suggerisce quale sia l'approccio giusto per ognuna. Scritto da una prospettiva C # ma applicabile in tutte le lingue, credo.
Damien_The_Unbeliever,

3
Credo che il codice violi il principio "Fail-fast". C'è un'alta probabilità che il bug reale sia nascosto in esso.
Euforico


4
Stai prendendo troppo. Anche tu cattureresti bug, come NRE, indice di array fuori limite, errore di configurazione, ... Ciò ti impedisce di trovare quei bug e causeranno continuamente danni.
usr

Risposte:


52

Immagina il codice con migliaia di file usando un mucchio di librerie. Immagina che tutti siano codificati in questo modo.

Immagina, ad esempio, un aggiornamento del tuo server fa scomparire un file di configurazione; e ora tutto ciò che hai è una traccia dello stack è un'eccezione puntatore null quando provi a usare quella classe: come lo risolveresti? Potrebbero essere necessarie ore, in cui almeno la registrazione della traccia di stack non elaborata del file non trovato [percorso file] potrebbe consentire di risolvere momentaneamente.

O peggio ancora: un errore in una delle librerie che usi dopo un aggiornamento che causa l'arresto anomalo del codice in un secondo momento. Come puoi rintracciarlo in biblioteca?

Anche senza una solida gestione degli errori

throw new IllegalStateException("THIS SHOULD NOT HAPPENING")

o

LOGGER.error("[method name]/[arguments] should not be there")

potrebbe farti risparmiare ore di tempo.

Ma ci sono alcuni casi in cui potresti davvero voler ignorare l'eccezione e restituire null in questo modo (o non fare nulla). Soprattutto se ti integri con un codice legacy mal progettato e questa eccezione è prevista come un caso normale.

In effetti, quando lo fai, dovresti semplicemente chiederti se stai davvero ignorando l'eccezione o "gestendo correttamente" le tue esigenze. Se restituire null significa "gestire correttamente" la tua eccezione in quel determinato caso, allora fallo. E aggiungi un commento perché questa è la cosa giusta da fare.

Le migliori pratiche sono le cose da seguire nella maggior parte dei casi, forse l'80%, forse il 99%, ma troverai sempre un caso limite in cui non si applicano. In tal caso, lascia un commento sul perché non stai seguendo la pratica per gli altri (o anche te stesso) che leggeranno il tuo codice mesi dopo.


7
+1 per spiegare perché pensi di doverlo fare in un commento. Senza una spiegazione, la deglutizione delle eccezioni avrebbe sempre suscitato nella mia testa campanelli d'allarme.
jpmc26,

Il mio problema è che il commento è bloccato all'interno di quel codice. Come consumatore della funzione, non potrei mai vedere quel commento.
corsiKa

@ jpmc26 Se l'eccezione viene gestita (ad esempio restituendo un valore speciale), ciò non significa affatto ingoiare eccezioni.
Deduplicatore,

2
@corsiKa un consumatore non ha bisogno di conoscere i dettagli dell'implementazione se la funzione sta svolgendo correttamente il proprio lavoro. forse sono parte di questo nelle librerie di Apache o Spring e non lo sai.
Walfrat,

@Deduplicator sì ma usare una cattura come parte del tuo algoritmo non dovrebbe essere una buona pratica anche :)
Walfrat

14

Ci sono casi in cui questo modello è utile, ma in genere vengono utilizzati quando l'eccezione generata non avrebbe mai dovuto essere (ovvero quando le eccezioni vengono utilizzate per il comportamento normale).

Ad esempio, immagina di avere una classe che apre un file, lo memorizza nell'oggetto che viene restituito. Se il file non esiste, è possibile considerare che non si tratta di un caso di errore, si restituisce null e si consente all'utente di creare un nuovo file. Il metodo di apertura dei file può generare un'eccezione qui per indicare che non è presente alcun file, quindi catturarlo in silenzio potrebbe essere una situazione valida.

Tuttavia, spesso non vorrai farlo, e se il codice è cosparso di un tale schema, vorrai affrontarlo ora. Per lo meno mi aspetterei che una cattura così silenziosa scriva una riga di registro dicendo che questo è quello che è successo (hai traccia, giusto) e un commento per spiegare questo comportamento.


2
"usato quando l'eccezione generata non avrebbe mai dovuto essere" nulla del genere. Il PUNTO di eccezione è segnalare che qualcosa è andato terribilmente storto.
Euforico,

3
@Euforico Ma a volte lo scrittore di una biblioteca non conosce le intenzioni dell'utente finale di quella biblioteca. Lo "orribilmente sbagliato" di una persona può essere la condizione facilmente recuperabile di qualcun altro.
Simon B,

2
@Euforico, dipende da ciò che il tuo linguaggio considera "orribilmente sbagliato": alla gente di Python piacciono le eccezioni per cose che sono molto vicine al controllo del flusso in (ad es.) C ++. La restituzione di null potrebbe essere difendibile in alcune situazioni, ad esempio la lettura da una cache in cui gli elementi potrebbero essere improvvisamente e imprevedibilmente sfrattati.
Matt Krause,

10
@Euforico, "Il file non trovato non è un'eccezione normale. Dovresti prima verificare se il file esiste se è possibile che non esista." No! No! No! No! Questo è il classico pensiero sbagliato riguardo alle operazioni atomiche. Il file potrebbe essere eliminato tra te controllando se esiste e accedendo ad esso. L'unico modo corretto per gestire tali situazioni è accedervi e gestire le eccezioni se si verificano, ad esempio restituendo nullse è la migliore lingua che la lingua scelta può offrire.
David Arno,

3
@Euforico: quindi, scrivi il codice per gestire non essere in grado di accedere al file almeno due volte? Ciò viola DRY, e anche il codice non esercitato è il codice non funzionante.
Deduplicatore,

7

Questo dipende al 100% dal contesto. Se il chiamante di tale funzione ha il requisito di visualizzare o registrare errori da qualche parte, allora è ovviamente senza senso. Se il chiamante ha ignorato tutti gli errori o tutti i messaggi di eccezione, non avrebbe molto senso restituire l'eccezione o il suo messaggio. Quando il requisito è di terminare il codice solo senza visualizzare alcun motivo, quindi una chiamata

   if(QueryServer(args)==null)
      TerminateProgram();

sarebbe probabilmente sufficiente. Devi considerare se questo renderà difficile trovare la causa dell'errore da parte dell'utente - in tal caso, questa è la forma sbagliata di gestione degli errori. Tuttavia, se il codice chiamante è simile al seguente

  result = QueryServer(args);
  if(result!=null)
      DoSomethingMeaningful(result);
  // else ??? Mask the error and let the user run into trouble later

quindi dovresti avere un argomento con lo sviluppatore durante la revisione del codice. Se qualcuno implementa una tale forma di gestione senza errori solo perché è troppo pigro per scoprire i requisiti corretti, allora questo codice non dovrebbe entrare in produzione e l'autore del codice dovrebbe essere istruito sulle possibili conseguenze di tale pigrizia .


5

Negli anni in cui ho trascorso la programmazione e lo sviluppo di sistemi, ci sono solo due situazioni in cui ho trovato utile lo schema in questione (in entrambi i casi la soppressione conteneva anche la registrazione dell'eccezione generata, non considero la semplice cattura e il nullritorno come una buona pratica ).

Le due situazioni sono le seguenti:

1. Quando l'eccezione non è stata considerata uno stato eccezionale

Questo è quando si esegue un'operazione su alcuni dati, che può essere lanciata, si sa che potrebbe essere lanciata, ma si desidera comunque che l'applicazione continui a funzionare, poiché non sono necessari i dati elaborati. Se li ricevi, è buono, se non lo fai, è anche buono.

Potrebbero venire in mente alcuni attributi opzionali di una classe.

2. Quando si fornisce una nuova (migliore, più veloce?) Implementazione di una libreria utilizzando un'interfaccia già utilizzata in un'applicazione

Immagina di avere un'applicazione che utilizza una sorta di vecchia libreria, che non ha nullgenerato eccezioni ma restituita per errore. Quindi hai creato un adattatore per questa libreria, praticamente copiando l'API originale della libreria e stai usando questa nuova interfaccia (ancora non lanciante) nella tua applicazione e gestendo nulltu stesso i controlli.

Viene una nuova versione della libreria, o forse una libreria completamente diversa che offre la stessa funzionalità, che, invece di restituire nulls, genera eccezioni e si desidera utilizzarla.

Non si desidera perdere le eccezioni all'applicazione principale, quindi è necessario sopprimerle e registrarle nell'adattatore creato per avvolgere questa nuova dipendenza.


Il primo caso non è un problema, è il comportamento desiderato del codice. Nella seconda situazione, tuttavia, se ovunque il nullvalore restituito dell'adattatore della libreria significhi davvero un errore, refactoring dell'API per generare un'eccezione e catturarla invece di verificare nullpotrebbe essere (e in genere il codice è una buona idea).

Personalmente uso la soppressione delle eccezioni solo per il primo caso. L'ho usato solo per il secondo caso, quando non avevamo il budget per far funzionare il resto dell'applicazione con le eccezioni anziché con nulls.


-1

Mentre sembra logico dire che i programmi dovrebbero rilevare solo le eccezioni che sanno gestire e che non possono assolutamente sapere come gestire le eccezioni che non erano previste dal programmatore, tale affermazione ignora il fatto che molte operazioni possono fallire in un numero quasi illimitato di modi che non hanno effetti collaterali e che in molti casi la corretta gestione della stragrande maggioranza di tali guasti sarà identica; i dettagli esatti del fallimento saranno irrilevanti, e di conseguenza non dovrebbe importare se il programmatore li ha anticipati.

Se, ad esempio, lo scopo di una funzione è leggere un file di documento in un oggetto adatto e creare una nuova finestra del documento per mostrare quell'oggetto o segnalare all'utente che il file non può essere letto, un tentativo di caricare un il file di documento non valido non dovrebbe arrestare in modo anomalo l'applicazione, ma dovrebbe invece mostrare un messaggio che indica il problema ma lasciare che il resto dell'applicazione continui a funzionare normalmente a meno che per qualche motivo il tentativo di caricare il documento abbia danneggiato lo stato di qualcos'altro nel sistema .

Fondamentalmente, la corretta gestione di un'eccezione dipenderà spesso meno dal tipo di eccezione che dal luogo in cui è stata generata; se una risorsa è protetta da un blocco di lettura-scrittura e viene generata un'eccezione all'interno di un metodo che ha acquisito il blocco per la lettura, il comportamento corretto dovrebbe generalmente essere quello di rilasciare il blocco poiché il metodo non può aver fatto nulla alla risorsa . Se viene generata un'eccezione mentre il blocco viene acquisito per la scrittura, il blocco deve essere spesso invalidato, poiché la risorsa protetta potrebbe essere in uno stato non valido; se la primitiva di blocco non ha uno stato "invalidato", si dovrebbe aggiungere un flag per tenere traccia di tale invalidamento. Rilasciare il blocco senza invalidarlo è un errore perché un altro codice potrebbe vedere l'oggetto protetto in uno stato non valido. Lasciare la serratura penzolante, tuttavia, non è una soluzione adeguata neanche. La soluzione giusta è invalidare il blocco in modo che qualsiasi tentativo in sospeso o futuro di acquisizione fallisca immediatamente.

Se si scopre che una risorsa non valida viene abbandonata prima che ci sia un tentativo di usarla, non c'è motivo per cui dovrebbe far cadere l'applicazione. Se una risorsa invalidata è fondamentale per il proseguimento dell'operazione di un'applicazione, è necessario che l'applicazione venga interrotta ma è probabile che l'invalidazione della risorsa accada. Il codice che ha ricevuto l'eccezione originale spesso non ha modo di sapere quale situazione si applichi, ma se invalida la risorsa può garantire che in entrambi i casi venga intrapresa l'azione corretta.


2
Questo non riesce a rispondere alla domanda perché la gestione di un'eccezione senza conoscere i dettagli dell'eccezione! = Consuma silenziosamente un'eccezione (generica).
Taemyr,

@Taemyr: se lo scopo di una funzione è "fare X se possibile, oppure indicare che non lo era", e X non era possibile, sarà spesso poco pratico elencare tutti i tipi di eccezioni che né la funzione né al chiamante interesserà oltre "Non è stato possibile fare X". La gestione delle eccezioni di Pokemon è spesso l'unico modo pratico per far funzionare le cose in questi casi, e non è necessario che sia così malvagia come fanno alcune persone se il codice è disciplinato sull'invalidazione di cose che vengono corrotte da eccezioni impreviste.
supercat,

Esattamente. Ogni metodo dovrebbe avere uno scopo, se può riempire il suo scopo indipendentemente dal fatto che un metodo che chiama genera o meno un'eccezione, quindi deglutire l'eccezione, qualunque essa sia, e fare il suo lavoro, è la cosa giusta da fare. La gestione degli errori si basa fondamentalmente sul completamento dell'attività dopo un errore. Questo è ciò che conta, non quale errore si è verificato.
jmoreno,

@jmoreno: Ciò che rende le cose difficili è che in molte situazioni ci saranno un gran numero di eccezioni, molte delle quali un programmatore non avrebbe particolarmente previsto, il che non indicherebbe un problema, e alcune eccezioni, alcune delle quali un programmatore non vorrebbe In particolare, anticipare, che indicano un problema, e nessun modo sistematico definito per distinguerli. L'unico modo che conosco per risolverlo in modo sano è che le eccezioni invalidino le cose che sono state corrotte, quindi se contano vengono notate e se non contano vengono ignorate.
supercat

-2

Ingoiare questo tipo di errore non è particolarmente utile per nessuno. Sì, al chiamante originale potrebbe non interessare l'eccezione ma qualcun altro potrebbe .

Allora cosa fanno? Aggiungeranno il codice per gestire l'eccezione per vedere quale tipo di eccezione stanno recuperando. Grande. Ma se viene lasciato il gestore delle eccezioni, il chiamante originale non viene più annullato e qualcosa si rompe altrove.

Anche se sei consapevole del fatto che un codice a monte potrebbe generare un errore e quindi restituire un valore nullo, sarebbe seriamente inerte per te non provare almeno a respingere l'eccezione nel codice di chiamata IMHO.


"seriamente inerte" - intendevi forse "seriamente inetto"?
Nome esoterico dello schermo

@EsotericScreenName Scegli uno ... ;-)
Robbie Dee

-3

Ho visto esempi in cui le librerie di terze parti avevano metodi potenzialmente utili, tranne per il fatto che in alcuni casi generano eccezioni che dovrebbero funzionare per me. Cosa posso fare?

  • Implementare esattamente ciò di cui ho bisogno di me stesso. Può essere difficile entro una scadenza.
  • Modifica il codice di terze parti. Ma poi devo cercare conflitti di unione con ogni aggiornamento.
  • Scrivi un metodo wrapper per trasformare l'eccezione in un normale puntatore null, un falso booleano o altro.

Ad esempio, il metodo della libreria

public foo findFoo() {...} 

restituisce il primo pippo, ma se non ce n'è uno genera un'eccezione. Ma quello di cui ho bisogno è un metodo per restituire il primo pippo o un null. Quindi scrivo questo:

public foo myFindFoo() {
    try {
        return findFoo() 
    } 
    catch (NoFooException ex) {
        return null;
    }
}

Non pulito. Ma a volte la soluzione pragmatica.


1
Cosa intendi per "generare eccezioni dove dovrebbero funzionare per te"?
corsiKa

@corsiKa, l'esempio che mi viene in mente sono i metodi di ricerca in un elenco o in una struttura di dati simile. Hanno falliscono quando trovano nulla o se invece restituiscono un valore nullo? Un altro esempio sono i metodi waitUntil che falliscono nel timeout anziché restituire un falso booleano.
om
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.