Perché è cattivo stile salvare `Exception => e` in Ruby?


895

Ruby QuickRef di Ryan Davis dice (senza spiegazione):

Non salvare l'eccezione. MAI. o ti pugnalerò.

Perchè no? Qual è la cosa giusta da fare?


35
Allora probabilmente potresti scriverne uno tuo? :)
Sergio Tulentsev,

65
Sono molto a disagio con la chiamata alla violenza qui. È solo programmazione.
Darth Egregious,

1
Dai un'occhiata a questo articolo in Ruby Exception con una bella gerarchia di Ruby Exception .
Atul Khanduri,

2
Perché Ryan Davis ti pugnalerà. Quindi ragazzi. Non salvare mai le eccezioni.
Mugen,

7
@DarthEgregious Non so davvero se stai scherzando o no. Ma penso che sia divertente. (E ovviamente non è una minaccia seria). Ora ogni volta che penso di catturare l'eccezione, penso se valga la pena di essere pugnalato da un ragazzo a caso su Internet.
Steve Sether,

Risposte:


1375

TL; DR : utilizzare StandardErrorinvece per la rilevazione generale delle eccezioni. Quando l'eccezione originale viene nuovamente sollevata (ad es. Durante il salvataggio per registrare solo l'eccezione), Exceptionè probabile che il salvataggio sia corretto.


Exceptionè la radice della gerarchia delle eccezioni di Ruby , così quando si rescue Exceptionsi salvataggio da tutto , tra cui sottoclassi, come SyntaxError, LoadErrore Interrupt.

Il salvataggio Interruptimpedisce all'utente di utilizzare CTRLCper uscire dal programma.

Il salvataggio SignalExceptionimpedisce al programma di rispondere correttamente ai segnali. Sarà ingiustificabile se non da kill -9.

Salvare SyntaxErrorsignifica questoeval i fallimenti lo faranno silenziosamente.

Tutti questi possono essere indicati da eseguire questo programma, e cercando di CTRLCo killesso:

loop do
  begin
    sleep 1
    eval "djsakru3924r9eiuorwju3498 += 5u84fior8u8t4ruyf8ihiure"
  rescue Exception
    puts "I refuse to fail or be stopped!"
  end
end

Il salvataggio da Exceptionnon è nemmeno il valore predefinito. fare

begin
  # iceberg!
rescue
  # lifeboats
end

non salva da Exception, salva da StandardError. Generalmente dovresti specificare qualcosa di più specifico del valore predefinito StandardError, ma il salvataggio Exception dall'ampliamento dell'ambito anziché restringerlo e può avere risultati catastrofici e rendere estremamente difficile la ricerca di bug.


Se hai una situazione in cui vuoi salvare StandardErrore hai bisogno di una variabile con l'eccezione, puoi usare questo modulo:

begin
  # iceberg!
rescue => e
  # lifeboats
end

che equivale a:

begin
  # iceberg!
rescue StandardError => e
  # lifeboats
end

Uno dei pochi casi comuni Exceptionin cui è opportuno eseguire il salvataggio è a scopo di registrazione / segnalazione, nel qual caso è necessario ripetere immediatamente l'eccezione:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end

129
quindi è come catturare Throwablejava
maniaco del cricchetto il

53
Questo consiglio è buono per un ambiente Ruby pulito. Ma sfortunatamente un certo numero di gemme ha creato eccezioni che discendono direttamente dall'eccezione. Il nostro ambiente ha 30 di questi: ad esempio OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Queste sono eccezioni che vorresti davvero catturare in blocchi di salvataggio standard. Sfortunatamente, nulla in Ruby impedisce o addirittura scoraggia le gemme di ereditare direttamente dall'eccezione - anche la denominazione non è intuitiva.
Jonathan Swartz,

20
@JonathanSwartz Quindi salva da quelle sottoclassi specifiche, non Eccezione. Più specifico è quasi sempre migliore e più chiaro.
Andrew Marshall,

22
@JonathanSwartz - Darei bug ai creatori di gemme per cambiare da cosa eredita la loro eccezione. Personalmente, mi piacciono le mie gemme perché tutte le eccezioni discendano da MyGemException, quindi puoi salvarlo se lo desideri.
Nathan Long,

12
Puoi anche ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]e poirescue *ADAPTER_ERRORS => e
j_mcnally,

83

La vera regola è: non buttare via le eccezioni. L'obiettività dell'autore della tua citazione è discutibile, come dimostra il fatto che termina con

o ti pugnalerò

Ovviamente, tieni presente che i segnali (per impostazione predefinita) generano eccezioni e che i processi di lunga durata vengono terminati attraverso un segnale, quindi catturare l'eccezione e non terminare con le eccezioni del segnale renderà il tuo programma molto difficile da interrompere. Quindi non farlo:

#! /usr/bin/ruby

while true do
  begin
    line = STDIN.gets
    # heavy processing
  rescue Exception => e
    puts "caught exception #{e}! ohnoes!"
  end
end

No, davvero, non farlo. Non eseguirlo nemmeno per vedere se funziona.

Tuttavia, supponi di avere un server thread e desideri che tutte le eccezioni non:

  1. essere ignorato (impostazione predefinita)
  2. arresta il server (cosa che succede se dici thread.abort_on_exception = true).

Quindi questo è perfettamente accettabile nel thread di gestione della connessione:

begin
  # do stuff
rescue Exception => e
  myLogger.error("uncaught #{e} exception while handling connection: #{e.message}")
    myLogger.error("Stack trace: #{backtrace.map {|l| "  #{l}\n"}.join}")
end

Quanto sopra risolve una variante del gestore delle eccezioni predefinito di Ruby, con il vantaggio che non uccide anche il tuo programma. Rails lo fa nel suo gestore di richieste.

Le eccezioni del segnale vengono sollevate nel thread principale. I thread in background non li avranno, quindi non ha senso cercare di catturarli lì.

Ciò è particolarmente utile in un ambiente di produzione, in cui non si desidera che il programma si interrompa semplicemente ogni volta che qualcosa va storto. Quindi è possibile prendere i dump dello stack nei registri e aggiungerli al codice per gestire l'eccezione specifica più in basso nella catena di chiamate e in modo più grazioso.

Nota anche che esiste un altro idioma di Ruby che ha più o meno lo stesso effetto:

a = do_something rescue "something else"

In questa linea, se do_somethingsolleva un'eccezione, viene catturato da Ruby, gettato via e aassegnato "something else".

In generale, non farlo, tranne in casi speciali in cui sai che non devi preoccuparti. Un esempio:

debugger rescue nil

Il debugger funzione è un modo piuttosto carino per impostare un punto di interruzione nel codice, ma se eseguito all'esterno di un debugger e Rails, genera un'eccezione. Ora teoricamente non dovresti lasciare il codice di debug in giro nel tuo programma (pff! Nessuno lo fa!) Ma potresti voler tenerlo lì per un po 'per qualche motivo, ma non eseguire continuamente il tuo debugger.

Nota:

  1. Se hai eseguito il programma di qualcun altro che rileva le eccezioni del segnale e le ignora (dì il codice sopra) quindi:

    • in Linux, in una shell, digitare pgrep rubyo ps | grep ruby, cercare il PID del programma offensivo, quindi eseguire kill -9 <PID>.
    • in Windows, utilizzare Task Manager ( CTRL- SHIFT- ESC), andare alla scheda "processi", trovare il processo, fare clic con il tasto destro del mouse e selezionare "Termina processo".
  2. Se stai lavorando con il programma di qualcun altro che, per qualsiasi motivo, è disseminato di questi blocchi di eccezione-ignore, mettere questo in cima alla linea principale è un possibile cop-out:

    %W/INT QUIT TERM/.each { |sig| trap sig,"SYSTEM_DEFAULT" }

    Questo fa sì che il programma risponda ai normali segnali di terminazione terminando immediatamente, bypassando i gestori delle eccezioni, senza pulizia . Quindi potrebbe causare la perdita di dati o simili. Stai attento!

  3. Se è necessario eseguire questa operazione:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    puoi effettivamente farlo:

    begin
      do_something
    ensure
      critical_cleanup
    end

    Nel secondo caso, critical cleanupverrà chiamato ogni volta, indipendentemente dal fatto che venga generata un'eccezione.


21
Siamo spiacenti, questo è sbagliato. Un server non dovrebbe mai salvare l'eccezione e non fare altro che registrarlo. Ciò lo renderà invendibile se non da kill -9.
Giovanni

8
I tuoi esempi nella nota 3 non sono equivalenti, ensureverrà eseguito indipendentemente dal fatto che vi sia o meno un'eccezione, mentre rescueverrà eseguito solo se è stata sollevata un'eccezione.
Andrew Marshall l'

1
Non sono / esattamente / equivalenti, ma non riesco a capire come esprimere sinteticamente l'equivalenza in un modo che non sia brutto.
Michael Slade,

3
Basta aggiungere un'altra chiamata critical_cleanup dopo il blocco di avvio / salvataggio nel primo esempio. Concordo non sul codice più elegante, ma ovviamente il secondo esempio è il modo elegante di farlo, quindi un po 'di ineleganza è solo una parte dell'esempio.
gtd

3
"Non eseguirlo nemmeno per vedere se funziona." sembra un cattivo consiglio per la codifica ... Al contrario, ti consiglierei di eseguirlo, di vederlo fallire e di capire da solo come se fallisse, invece di credere ciecamente a qualcun altro. Ottima risposta comunque :)
huelbois

69

TL; DR

Non farlo rescue Exception => e(e non aumentare nuovamente l'eccezione), altrimenti potresti scappare da un ponte.


Diciamo che sei in macchina (con Ruby). Di recente hai installato un nuovo volante con il sistema di aggiornamento over-the-air (che utilizza eval), ma non sapevi che uno dei programmatori ha incasinato la sintassi.

Sei su un ponte e ti rendi conto che stai andando un po 'verso la ringhiera, quindi giri a sinistra.

def turn_left
  self.turn left:
end

oops! Questo è probabilmente Not Good ™, per fortuna, Ruby solleva a SyntaxError.

L'auto dovrebbe fermarsi immediatamente - giusto?

No.

begin
  #...
  eval self.steering_wheel
  #...
rescue Exception => e
  self.beep
  self.log "Caught #{e}.", :warn
  self.log "Logged Error - Continuing Process.", :info
end

Beep Beep

Avviso: Eccezione errore di sintassi rilevata.

Informazioni: errore registrato - processo continuo.

Si nota che qualcosa non va, e si sbattere sulle rotture di emergenza ( ^C: Interrupt)

Beep Beep

Avvertenza: eccezione di interruzione rilevata.

Informazioni: errore registrato - processo continuo.

Sì, non è stato di grande aiuto. Sei abbastanza vicino al parapetto, quindi metti la macchina nel parcheggio ( killing:) SignalException.

Beep Beep

Avviso: eccezione rilevata da SignalException.

Informazioni: errore registrato - processo continuo.

All'ultimo secondo, tiri fuori le chiavi ( kill -9) e l'auto si ferma, sbatti in avanti sul volante (l'airbag non può gonfiarsi perché non hai fermato con grazia il programma - l'hai terminato) e il computer nella parte posteriore della tua auto sbatte contro il sedile di fronte. Una lattina piena di coca cola tra le carte. I generi alimentari nella parte posteriore sono schiacciati e la maggior parte sono ricoperti di tuorlo d'uovo e latte. L'auto necessita di seri interventi di riparazione e pulizia. (Perdita di dati)

Spero che tu abbia un'assicurazione (backup). Oh sì - perché l'airbag non si è gonfiato, probabilmente ti sei fatto male (essere licenziato, ecc.).


Ma aspetta! C'èDi Piùmotivi per cui potresti voler usare rescue Exception => e!

Supponiamo che tu sia quella macchina e che tu voglia assicurarti che l'airbag si gonfi se la macchina sta superando il suo momento di arresto sicuro.

 begin 
    # do driving stuff
 rescue Exception => e
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
    raise
 end

Ecco l'eccezione alla regola: puoi intercettare Exception solo se riduci l'eccezione . Quindi, una regola migliore è quella di non ingoiare mai e di rievocare Exceptionsempre l'errore.

Ma aggiungere il salvataggio è sia facile da dimenticare in una lingua come Ruby, sia mettere una dichiarazione di salvataggio prima di sollevare nuovamente un problema sembra un po 'non ASCIUTTO. E non vuoi dimenticare l' raiseaffermazione. E se lo fai, buona fortuna cercando di trovare quell'errore.

Per fortuna, Ruby è fantastico, puoi semplicemente usare la ensureparola chiave, che assicura che il codice venga eseguito. La ensureparola chiave eseguirà il codice in ogni caso - se viene generata un'eccezione, in caso contrario, l'unica eccezione è se il mondo finisce (o altri eventi improbabili).

 begin 
    # do driving stuff
 ensure
    self.airbags.inflate if self.exceeding_safe_stopping_momentum?
 end

Boom! E quel codice dovrebbe essere eseguito comunque. L'unico motivo da utilizzare rescue Exception => eè se è necessario accedere all'eccezione o se si desidera eseguire solo il codice su un'eccezione. E ricorda di aumentare nuovamente l'errore. Ogni volta.

Nota: come sottolineato da @Niall, assicurarsi che funzioni sempre . Questo è utile perché a volte il tuo programma può mentirti e non generare eccezioni, anche quando si verificano problemi. Con attività critiche, come il gonfiaggio degli airbag, è necessario assicurarsi che ciò accada, qualunque cosa accada. Per questo motivo, controllare ogni volta che l'auto si ferma, se viene generata un'eccezione o meno, è una buona idea. Anche se gonfiare gli airbag è un compito un po 'insolito nella maggior parte dei contesti di programmazione, questo è in realtà abbastanza comune con la maggior parte delle attività di pulizia.


12
Hahahaha! Questa è un'ottima risposta Sono scioccato dal fatto che nessuno abbia commentato. Dai uno scenario chiaro che rende tutto comprensibile. Saluti! :-)
James Milani,

@JamesMilani Grazie!
Ben Aubin,

3
+ 💯 per questa risposta. Vorrei poter votare più di una volta! 😂
engineerDave,

1
Buona risposta!
Atul Vaibhav,

3
Questa risposta è arrivata 4 anni dopo la risposta accettata perfettamente comprensibile e corretta, e l'ha spiegata di nuovo con uno scenario assurdo progettato più per essere divertente che realistico. Mi dispiace essere un buzzkill, ma questo non è reddit - è più importante che le risposte siano succinte e corrette piuttosto che divertenti. Inoltre, la parte ensurein alternativa a rescue Exceptionè fuorviante - l'esempio implica che sono equivalenti, ma come affermato ensureaccadrà se c'è un'eccezione o no, quindi ora i tuoi airbag si gonfieranno perché hai superato i 5 miglia all'ora anche se nulla è andato storto.
Niall,

47

Perché questo cattura tutte le eccezioni. È improbabile che il tuo programma possa recuperare da uno qualsiasi di essi.

Dovresti gestire solo le eccezioni da cui sai come recuperare. Se non prevedi un certo tipo di eccezione, non gestirla, arresta in modo anomalo (scrivi i dettagli nel log), quindi diagnostica i log e correggi il codice.

Ingoiare le eccezioni è male, non farlo.


10

Questo è un caso specifico della regola che non dovresti prendere alcuna eccezione che non sai come gestire. Se non sai come gestirlo, è sempre meglio lasciare che un'altra parte del sistema lo catturi e lo gestisca.


0

Ho appena letto un fantastico post sul blog su honeybadger.io :

Ruby Exception vs StandardError: Qual è la differenza?

Perché non dovresti salvare l'eccezione

Il problema con il salvataggio dell'eccezione è che in realtà salva ogni eccezione ereditata dall'eccezione. Che è .... tutti loro!

Questo è un problema perché ci sono alcune eccezioni utilizzate internamente da Ruby. Non hanno nulla a che fare con la tua app e la loro ingestione causerà cose brutte.

Ecco alcuni dei più grandi:

  • SignalException :: Interrupt: se lo salvi, non puoi uscire dall'app premendo control-c.

  • ScriptError :: SyntaxError - L'ingestione di errori di sintassi significa che cose come put ("Forgot qualcosa) falliranno silenziosamente.

  • NoMemoryError - Vuoi sapere cosa succede quando il tuo programma continua a funzionare dopo che utilizza tutta la RAM? Neanche io.

begin
  do_something()
rescue Exception => e
  # Don't do this. This will swallow every single exception. Nothing gets past it. 
end

Immagino che tu non voglia davvero ingoiare nessuna di queste eccezioni a livello di sistema. Desideri solo rilevare tutti gli errori a livello di applicazione. Le eccezioni hanno causato il TUO codice.

Fortunatamente, c'è un modo semplice per farlo.

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.