Warum ist es schlecht, in Ruby "Exception => e" zu retten?

895

Ryan Davis ' Ruby QuickRef sagt (ohne Erklärung):

Rettung der Ausnahme nicht. JE. oder ich werde dich erstechen.

Warum nicht? Was ist das Richtige?

John
quelle
35
Dann könnten Sie wahrscheinlich Ihre eigenen schreiben? :)
Sergio Tulentsev
65
Der Aufruf zur Gewalt hier ist mir sehr unangenehm. Es ist nur Programmierung.
Darth Egregious
1
Schauen Sie sich diesen Artikel in Ruby Exception mit einer schönen Ruby Exception-Hierarchie an .
Atul Khanduri
2
Weil Ryan Davis dich erstechen wird. Also Kinder. Rette niemals Ausnahmen.
Mugen
7
@DarthEgregious Ich kann nicht wirklich sagen, ob du Witze machst oder nicht. Aber ich finde es lustig. (Und es ist offensichtlich keine ernsthafte Bedrohung). Jedes Mal, wenn ich daran denke, eine Ausnahme zu erwischen, überlege ich, ob es sich lohnt, von einem zufälligen Typen im Internet erstochen zu werden.
Steve Sether

Antworten:

1375

TL; DR : Verwenden Sie StandardErrorstattdessen für das allgemeine Abfangen von Ausnahmen. Wenn die ursprüngliche Ausnahme erneut ausgelöst wird (z. B. beim Speichern, um nur die Ausnahme zu protokollieren), Exceptionist das Speichern wahrscheinlich in Ordnung.


Exceptionist die Wurzel der Ausnahme Hierarchie Ruby so, wenn du rescue Exceptiondich von Rettung alles , einschließlich der Unterklassen wie SyntaxError, LoadError, und Interrupt.

Die Rettung Interruptverhindert, dass der Benutzer CTRLCdas Programm beendet.

Die Rettung SignalExceptionverhindert, dass das Programm korrekt auf Signale reagiert. Es wird nicht tötbar sein, außer durch kill -9.

Rettung SyntaxErrorbedeutet, dass evals, die fehlschlagen, dies stillschweigend tun.

All dies kann angezeigt werden, indem Sie dieses Programm ausführen und versuchen, es CTRLCoder killes:

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

Die Rettung von Exceptionist nicht einmal die Standardeinstellung. Tun

begin
  # iceberg!
rescue
  # lifeboats
end

Exceptionrettet nicht vor , es rettet vor StandardError. Sie sollten im Allgemeinen etwas Spezifischeres als die Standardeinstellung angeben StandardError, aber die Rettung aus dem Bereich Exception erweitert den Bereich, anstatt ihn einzuschränken, und kann katastrophale Folgen haben und die Fehlersuche äußerst schwierig machen.


Wenn Sie eine Situation haben, aus der StandardErrorSie retten möchten, und Sie mit Ausnahme der Ausnahme eine Variable benötigen, können Sie dieses Formular verwenden:

begin
  # iceberg!
rescue => e
  # lifeboats
end

was äquivalent ist zu:

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

Einer der wenigen Fälle, in denen eine Rettung sinnvoll ist, Exceptionist die Protokollierung / Berichterstellung. In diesem Fall sollten Sie die Ausnahme sofort erneut auslösen:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Andrew Marshall
quelle
129
so ist es wie Throwablein Java zu fangen
Ratschenfreak
53
Dieser Rat ist gut für eine saubere Ruby-Umgebung. Leider haben einige Edelsteine ​​Ausnahmen geschaffen, die direkt von der Ausnahme abstammen. Unsere Umgebung verfügt über 30 davon: z. B. OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Dies sind Ausnahmen, die Sie unbedingt in Standard-Rettungsblöcken abfangen möchten. Leider verhindert oder hält nichts in Ruby Edelsteine ​​davon ab, direkt von Exception zu erben - selbst die Benennung ist nicht intuitiv.
Jonathan Swartz
20
@ JonathanSwartz Dann rette aus diesen spezifischen Unterklassen, nicht aus Ausnahme. Genauer ist fast immer besser und klarer.
Andrew Marshall
22
@ JonathanSwartz - Ich würde die Edelsteinersteller nerven, um zu ändern, wovon ihre Ausnahme erbt. Persönlich mag ich es, wenn meine Edelsteine ​​alle Ausnahmen von MyGemException haben, sodass Sie das retten können, wenn Sie möchten.
Nathan Long
12
Sie können auch ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]und dannrescue *ADAPTER_ERRORS => e
j_mcnally
83

Die eigentliche Regel lautet: Werfen Sie keine Ausnahmen weg. Die Objektivität des Autors Ihres Zitats ist fraglich, wie die Tatsache zeigt, dass es mit endet

oder ich werde dich erstechen

Beachten Sie natürlich, dass Signale (standardmäßig) Ausnahmen auslösen und normalerweise lang laufende Prozesse über ein Signal beendet werden. Wenn Sie also Exception abfangen und nicht bei Signalausnahmen beenden, ist Ihr Programm sehr schwer zu stoppen. Also mach das nicht:

#! /usr/bin/ruby

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

Nein, wirklich, tu es nicht. Führen Sie das nicht einmal aus, um zu sehen, ob es funktioniert.

Angenommen, Sie haben einen Thread-Server und möchten, dass alle Ausnahmen nicht:

  1. ignoriert werden (die Standardeinstellung)
  2. Stoppen Sie den Server (was passiert, wenn Sie sagen thread.abort_on_exception = true).

Dann ist dies in Ihrem Verbindungsbehandlungs-Thread vollkommen akzeptabel:

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

Das Obige funktioniert mit einer Variation von Rubys Standard-Ausnahmebehandlungsroutine, mit dem Vorteil, dass Ihr Programm dadurch nicht ebenfalls beendet wird. Rails tut dies in seinem Request-Handler.

Signalausnahmen werden im Haupt-Thread ausgelöst. Hintergrund-Threads werden sie nicht bekommen, daher macht es keinen Sinn, sie dort zu fangen.

Dies ist besonders nützlich in einer Produktionsumgebung, wo Sie nicht stoppen Ihr Programm wollen einfach , wenn etwas schief geht. Dann können Sie die Stack-Dumps in Ihre Protokolle aufnehmen und zu Ihrem Code hinzufügen, um bestimmte Ausnahmen weiter unten in der Aufrufkette und auf elegantere Weise zu behandeln.

Beachten Sie auch, dass es eine andere Ruby-Sprache gibt, die den gleichen Effekt hat:

a = do_something rescue "something else"

Wenn in dieser Zeile do_somethingeine Ausnahme ausgelöst wird, wird sie von Ruby abgefangen, weggeworfen und azugewiesen "something else".

Tun Sie dies im Allgemeinen nicht, außer in besonderen Fällen, in denen Sie wissen, dass Sie sich keine Sorgen machen müssen. Ein Beispiel:

debugger rescue nil

Die debuggerFunktion ist eine gute Möglichkeit, einen Haltepunkt in Ihrem Code festzulegen. Wenn Sie jedoch außerhalb eines Debuggers und von Rails ausgeführt werden, wird eine Ausnahme ausgelöst. Theoretisch sollten Sie den Debugger-Code nicht in Ihrem Programm herumliegen lassen (pff! Niemand macht das!), Aber Sie möchten ihn möglicherweise aus irgendeinem Grund für eine Weile dort lassen, aber Ihren Debugger nicht kontinuierlich ausführen.

Hinweis:

  1. Wenn Sie ein anderes Programm ausgeführt haben, das Signalausnahmen abfängt und ignoriert (sagen Sie den obigen Code), dann:

    • Geben Sie unter Linux in einer Shell die PID Ihres fehlerhaften Programms ein pgrep rubyoder ps | grep rubysuchen Sie sie und führen Sie sie aus kill -9 <PID>.
    • Verwenden Sie in Windows den Task-Manager ( CTRL- SHIFT- ESC), wechseln Sie zur Registerkarte "Prozesse", suchen Sie Ihren Prozess, klicken Sie mit der rechten Maustaste darauf und wählen Sie "Prozess beenden".
  2. Wenn Sie mit einem anderen Programm arbeiten, das aus irgendeinem Grund mit diesen Blöcken zum Ignorieren von Ausnahmen gespickt ist, ist es eine mögliche Ausrede, dies an die Spitze der Hauptzeile zu setzen:

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

    Dies bewirkt, dass das Programm auf die normalen Beendigungssignale reagiert, indem es Ausnahmebehandler ohne Bereinigung sofort beendet und umgeht . Dies kann zu Datenverlust oder Ähnlichem führen. Achtung!

  3. Wenn Sie dies tun müssen:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    Sie können dies tatsächlich tun:

    begin
      do_something
    ensure
      critical_cleanup
    end

    Im zweiten Fall critical cleanupwird jedes Mal aufgerufen, ob eine Ausnahme ausgelöst wird oder nicht.

Michael Slade
quelle
21
Entschuldigung, das ist falsch. Ein Server sollte Exception niemals retten und nichts anderes tun, als es zu protokollieren. Das wird es nicht tötbar machen, außer durch kill -9.
John
8
Ihre Beispiele in Anmerkung 3 sind nicht gleichberechtigt. Sie ensurewerden unabhängig davon ausgeführt, ob eine Ausnahme ausgelöst wurde oder nicht, während die rescueAusführung nur ausgeführt wird, wenn eine Ausnahme ausgelöst wurde.
Andrew Marshall
1
Sie sind nicht / genau / gleichwertig, aber ich kann nicht herausfinden, wie ich die Gleichwertigkeit auf eine nicht hässliche Weise prägnant ausdrücken kann.
Michael Slade
3
Fügen Sie einfach nach dem Start- / Rettungsblock im ersten Beispiel einen weiteren Aufruf von static_cleanup hinzu. Ich stimme nicht dem elegantesten Code zu, aber offensichtlich ist das zweite Beispiel die elegante Art, dies zu tun, so dass ein wenig Uneleganz nur ein Teil des Beispiels ist.
GTD
3
"Führen Sie das nicht einmal aus, um zu sehen, ob es funktioniert." scheint ein schlechter Rat für das Codieren zu sein ... Im Gegenteil, ich würde Ihnen raten, es auszuführen, zu sehen, dass es fehlschlägt, und selbst zu verstehen, wie es fehlschlägt, anstatt blind jemand anderem zu glauben. Tolle Antwort trotzdem :)
Huelbois
69

TL; DR

Nicht rescue Exception => e(und die Ausnahme nicht erneut auslösen) - oder Sie könnten von einer Brücke fahren.


Angenommen, Sie sitzen in einem Auto (mit Ruby). Sie haben kürzlich ein neues Lenkrad mit dem drahtlosen Upgrade-System (das verwendet eval) installiert , aber Sie wussten nicht, dass einer der Programmierer die Syntax durcheinander gebracht hat.

Sie befinden sich auf einer Brücke und stellen fest, dass Sie ein Stück in Richtung Geländer fahren. Biegen Sie also links ab.

def turn_left
  self.turn left:
end

Hoppla! Das ist wahrscheinlich Not Good ™, zum Glück erhöht Ruby a SyntaxError.

Das Auto sollte sofort anhalten - richtig?

Nee.

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

Piep Piep

Warnung: SyntaxError-Ausnahme abgefangen.

Info: Protokollierter Fehler - Prozess wird fortgesetzt.

Man merkt , etwas falsch ist , und Sie Slam auf die Notbremse ( ^C: Interrupt)

Piep Piep

Warnung: Unterbrechungsausnahme abgefangen.

Info: Protokollierter Fehler - Prozess wird fortgesetzt.

Ja - das hat nicht viel geholfen. Du bist ziemlich nah an der Schiene, also stellst du das Auto in den Park ( killing :) SignalException.

Piep Piep

Warnung: SignalException Exception abgefangen.

Info: Protokollierter Fehler - Prozess wird fortgesetzt.

In der letzten Sekunde ziehen Sie die Schlüssel ( kill -9) heraus und das Auto hält an, Sie knallen nach vorne in das Lenkrad (der Airbag kann sich nicht aufblasen, weil Sie das Programm nicht ordnungsgemäß gestoppt haben - Sie haben es beendet) und den Computer auf der Rückseite Ihres Autos knallt auf den Sitz davor. Eine halb volle Dose Cola läuft über die Papiere. Die Lebensmittel auf der Rückseite sind zerkleinert und die meisten sind mit Eigelb und Milch bedeckt. Das Auto muss dringend repariert und gereinigt werden. (Datenverlust)

Hoffentlich haben Sie eine Versicherung (Backups). Oh ja - weil sich der Airbag nicht aufgeblasen hat, sind Sie wahrscheinlich verletzt (gefeuert werden usw.).


Aber warte! Es gibtMehrGründe, warum Sie verwenden möchten rescue Exception => e!

Nehmen wir an, Sie sind dieses Auto und möchten sicherstellen, dass sich der Airbag aufbläst, wenn das Auto seinen sicheren Bremsimpuls überschreitet.

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

Hier ist die Ausnahme von der Regel: Sie können Exception nur fangen , wenn Sie die Ausnahme erneut auslösen . Eine bessere Regel ist es also, niemals zu schlucken Exceptionund den Fehler immer wieder zu erhöhen.

Das Hinzufügen von Rettung ist in einer Sprache wie Ruby leicht zu vergessen, und das Setzen einer Rettungserklärung direkt vor dem erneuten Ansprechen eines Problems fühlt sich ein wenig nicht trocken an. Und Sie wollen die Aussage nicht vergessen raise. Und wenn Sie dies tun, viel Glück beim Versuch, diesen Fehler zu finden.

Zum Glück ist Ruby fantastisch. Sie können einfach das ensureSchlüsselwort verwenden, um sicherzustellen, dass der Code ausgeführt wird. Das ensureSchlüsselwort führt den Code aus, egal was passiert - wenn eine Ausnahme ausgelöst wird, wenn dies nicht der Fall ist, ist die einzige Ausnahme, wenn die Welt endet (oder andere unwahrscheinliche Ereignisse).

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

Boom! Und dieser Code sollte sowieso laufen. Der einzige Grund, den Sie verwenden sollten, rescue Exception => eist, wenn Sie Zugriff auf die Ausnahme benötigen oder wenn nur Code für eine Ausnahme ausgeführt werden soll. Und denken Sie daran, den Fehler erneut auszulösen. Jedes Mal.

Hinweis: Wie @Niall hervorhob, stellen Sie sicher, dass immer ausgeführt wird . Dies ist gut, da Ihr Programm Sie manchmal anlügen und keine Ausnahmen auslösen kann, selbst wenn Probleme auftreten. Bei kritischen Aufgaben wie dem Aufblasen von Airbags müssen Sie sicherstellen, dass dies auf jeden Fall geschieht. Aus diesem Grund ist es eine gute Idee, bei jedem Anhalten des Autos zu überprüfen, ob eine Ausnahme ausgelöst wird oder nicht. Obwohl das Aufblasen von Airbags in den meisten Programmierkontexten eine ungewöhnliche Aufgabe ist, ist dies bei den meisten Reinigungsaufgaben tatsächlich ziemlich häufig.

Ben Aubin
quelle
12
Hahahaha! Dies ist eine großartige Antwort. Ich bin schockiert, dass niemand etwas kommentiert hat. Sie geben ein klares Szenario an, das das Ganze wirklich verständlich macht. Prost! :-)
James Milani
@ JamesMilani Danke!
Ben Aubin
3
+ 💯 für diese Antwort. Ich wünschte, ich könnte mehr als einmal abstimmen! 😂
IngenieurDave
1
Genossen Ihre Antwort!
Atul Vaibhav
3
Diese Antwort kam 4 Jahre nach der vollkommen verständlichen und korrekt akzeptierten Antwort und erklärte sie erneut mit einem absurden Szenario, das eher amüsant als realistisch sein sollte. Es tut mir leid, ein Buzzkill zu sein, aber das ist kein Reddit - es ist wichtiger, dass die Antworten prägnant und korrekt sind als lustig. Auch der Teil ensureals Alternative zu rescue Exceptionist irreführend - das Beispiel impliziert, dass sie gleichwertig sind, aber wie angegeben ensurewird es passieren, ob es eine Ausnahme gibt oder nicht. Jetzt werden Ihre Airbags aufgeblasen, weil Sie über 8 km / h gefahren sind, obwohl nichts schief gelaufen ist.
Niall
47

Weil dies alle Ausnahmen erfasst. Es ist unwahrscheinlich, dass sich Ihr Programm von einem dieser Programme erholen kann.

Sie sollten nur Ausnahmen behandeln, von denen Sie wissen, wie Sie sie wiederherstellen können. Wenn Sie eine bestimmte Art von Ausnahme nicht erwarten, behandeln Sie sie nicht, stürzen Sie laut ab (schreiben Sie Details in das Protokoll), diagnostizieren Sie dann Protokolle und korrigieren Sie den Code.

Ausnahmen zu schlucken ist schlecht, tu das nicht.

Sergio Tulentsev
quelle
10

Dies ist ein spezieller Fall der Regel, dass Sie keine Ausnahme abfangen sollten, mit der Sie nicht umgehen können. Wenn Sie nicht wissen, wie Sie damit umgehen sollen, ist es immer besser, einen anderen Teil des Systems abfangen und damit umgehen zu lassen.

Russell Borogove
quelle
0

Ich habe gerade einen großartigen Blog-Beitrag darüber auf Honeybadger.io gelesen :

Rubys Ausnahme gegen StandardError: Was ist der Unterschied?

Warum sollten Sie Exception nicht retten?

Das Problem bei der Rettung von Exception besteht darin, dass tatsächlich jede von Resception geerbte Ausnahme gerettet wird. Welches ist ... alle von ihnen!

Dies ist ein Problem, da es einige Ausnahmen gibt, die von Ruby intern verwendet werden. Sie haben nichts mit Ihrer App zu tun, und wenn Sie sie verschlucken, passieren schlimme Dinge.

Hier sind einige der großen:

  • SignalException :: Interrupt - Wenn Sie dies beheben, können Sie Ihre App nicht beenden, indem Sie Control-c drücken.

  • ScriptError :: SyntaxError - Das Verschlucken von Syntaxfehlern bedeutet, dass Dinge wie Puts ("Etwas vergessen") stillschweigend fehlschlagen.

  • NoMemoryError - Möchten Sie wissen, was passiert, wenn Ihr Programm weiter ausgeführt wird, nachdem es den gesamten Arbeitsspeicher verbraucht hat? Ich auch nicht.

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

Ich vermute, dass Sie keine dieser Ausnahmen auf Systemebene wirklich schlucken möchten. Sie möchten nur alle Fehler auf Anwendungsebene abfangen. Die Ausnahmen verursachten IHREN Code.

Zum Glück gibt es einen einfachen Weg dorthin.

calebkm
quelle