¿Por qué es un mal estilo `rescatar Exception => e` en Ruby?

895

Ruby QuickRef de Ryan Davis dice (sin explicación):

No rescates la excepción. SIEMPRE. o te apuñalaré.

Por qué no? ¿Qué es lo que hay que hacer?

Juan
fuente
35
¿Entonces probablemente podrías escribir el tuyo? :)
Sergio Tulentsev
65
Estoy muy incómodo con el llamado a la violencia aquí. Es solo programación.
Darth Egregious
1
Echa un vistazo a este artículo en Ruby Exception con una bonita jerarquía de Ruby Exception .
Atul Khanduri
2
Porque Ryan Davis te apuñalará. Entonces niños. Nunca rescates excepciones.
Mugen
77
@DarthEgregious Realmente no puedo decir si estás bromeando o no. Pero creo que es gracioso. (Y obviamente no es una amenaza seria). Ahora, cada vez que pienso en atrapar Exception, considero si vale la pena ser apuñalado por algún tipo aleatorio en Internet.
Steve Sether

Respuestas:

1375

TL; DR : utilice en su StandardErrorlugar para la captura de excepciones generales. Cuando se vuelve a generar la excepción original (por ejemplo, cuando se rescata para registrar solo la excepción), el rescate Exceptionprobablemente esté bien.


Exceptiones la raíz de la jerarquía de excepciones de Ruby , por lo que al rescue Exceptionrescatar de todo , incluyendo las subclases tales como SyntaxError, LoadError, y Interrupt.

El rescate Interruptevita que el usuario use CTRLCpara salir del programa.

El rescate SignalExceptionevita que el programa responda correctamente a las señales. Será invencible excepto por kill -9.

Rescatar SyntaxErrorsignifica que evallos que fallan lo harán en silencio.

Todo esto se puede mostrar ejecutando este programa e intentando CTRLCo kill:

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

Rescatar de Exceptionni siquiera es el valor predeterminado. Haciendo

begin
  # iceberg!
rescue
  # lifeboats
end

no rescata Exception, rescata de StandardError. En general, debe especificar algo más específico que el predeterminado StandardError, pero rescatarlo Exception amplía el alcance en lugar de reducirlo, y puede tener resultados catastróficos y hacer que la búsqueda de errores sea extremadamente difícil.


Si tiene una situación en la que desea rescatar StandardErrory necesita una variable con la excepción, puede usar este formulario:

begin
  # iceberg!
rescue => e
  # lifeboats
end

que es equivalente a:

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

Uno de los pocos casos comunes en los que es sensato rescatar Exceptiones para fines de registro / informes, en cuyo caso debe volver a plantear la excepción de inmediato:

begin
  # iceberg?
rescue Exception => e
  # do some logging
  raise # not enough lifeboats ;)
end
Andrew Marshall
fuente
129129
así que es como atrapar Throwableen Java
monstruo de trinquete
53
Este consejo es bueno para un ambiente limpio de Ruby. Pero desafortunadamente, varias gemas han creado excepciones que descienden directamente de Exception. Nuestro entorno tiene 30 de estos: por ejemplo, OpenID :: Server :: EncodingError, OAuth :: InvalidRequest, HTMLTokenizerSample. Estas son excepciones que te gustaría atrapar en los bloques de rescate estándar. Desafortunadamente, nada en Ruby impide o incluso desalienta a las gemas a heredar directamente de Exception, incluso el nombramiento no es intuitivo.
Jonathan Swartz
20
@JonathanSwartz Luego rescata de esas subclases específicas, no Exception. Más específico es casi siempre mejor y más claro.
Andrew Marshall
22
@JonathanSwartz: molestaría a los creadores de gemas para cambiar de qué hereda su excepción. Personalmente, me gusta que mis gemas tengan todas las excepciones que descienden de MyGemException, para que pueda rescatar eso si lo desea.
Nathan Long
12
También puede ADAPTER_ERRORS = [::ActiveRecord::StatementInvalid, PGError, Mysql::Error, Mysql2::Error, ::ActiveRecord::JDBCError, SQLite3::Exception]y luegorescue *ADAPTER_ERRORS => e
j_mcnally
83

La verdadera regla es: no descarte las excepciones. La objetividad del autor de su cita es cuestionable, como lo demuestra el hecho de que termina con

o te apuñalaré

Por supuesto, tenga en cuenta que las señales (por defecto) arrojan excepciones, y normalmente los procesos de ejecución prolongada se terminan a través de una señal, por lo que capturar la excepción y no terminar en las excepciones de la señal hará que su programa sea muy difícil de detener. Entonces no hagas esto:

#! /usr/bin/ruby

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

No, en serio, no lo hagas. Ni siquiera ejecutes eso para ver si funciona.

Sin embargo, supongamos que tiene un servidor con subprocesos y desea que todas las excepciones no:

  1. ser ignorado (el valor predeterminado)
  2. Detenga el servidor (lo que sucede si usted lo dice thread.abort_on_exception = true).

Entonces esto es perfectamente aceptable en su hilo de manejo de conexión:

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

Lo anterior funciona con una variación del controlador de excepciones predeterminado de Ruby, con la ventaja de que tampoco mata su programa. Rails hace esto en su manejador de solicitudes.

Las excepciones de señal se generan en el hilo principal. Los hilos de fondo no los conseguirán, por lo que no tiene sentido tratar de atraparlos allí.

Esto es particularmente útil en un entorno de producción, donde no desea que su programa simplemente se detenga cuando algo sale mal. Luego puede tomar los volcados de la pila en sus registros y agregarlos a su código para lidiar con excepciones específicas más adelante en la cadena de llamadas y de una manera más elegante.

Tenga en cuenta también que hay otra expresión de Ruby que tiene el mismo efecto:

a = do_something rescue "something else"

En esta línea, si do_somethingse produce una excepción, Ruby lo atrapa, lo tira y alo asigna "something else".

En general, no haga eso, excepto en casos especiales en los que sabe que no necesita preocuparse. Un ejemplo:

debugger rescue nil

La debuggerfunción es una forma bastante agradable de establecer un punto de interrupción en su código, pero si se ejecuta fuera de un depurador y Rails, genera una excepción. Ahora, en teoría, no deberías dejar el código de depuración en tu programa (¡pff! ¡Nadie hace eso!), Pero es posible que quieras mantenerlo allí por un tiempo por alguna razón, pero no ejecutes continuamente tu depurador.

Nota:

  1. Si ha ejecutado el programa de otra persona que detecta excepciones de señales y las ignora (diga el código anterior), entonces:

    • en Linux, en un shell, escriba pgrep rubyo ps | grep rubybusque el PID de su programa infractor y luego ejecútelo kill -9 <PID>.
    • en Windows, use el Administrador de tareas ( CTRL- SHIFT- ESC), vaya a la pestaña "procesos", busque su proceso, haga clic con el botón derecho y seleccione "Finalizar proceso".
  2. Si está trabajando con el programa de otra persona que, por cualquier motivo, está salpicado de estos bloques de ignorar excepciones, poner esto en la parte superior de la línea principal es una posible solución:

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

    Esto hace que el programa responda a las señales de terminación normales al terminar inmediatamente, sin pasar por los controladores de excepciones, sin limpieza . Por lo tanto, podría causar pérdida de datos o similar. ¡Ten cuidado!

  3. Si necesitas hacer esto:

    begin
      do_something
    rescue Exception => e
      critical_cleanup
      raise
    end

    en realidad puedes hacer esto:

    begin
      do_something
    ensure
      critical_cleanup
    end

    En el segundo caso, critical cleanupse llamará cada vez, se produzca o no una excepción.

Michael Slade
fuente
21
Lo siento, esto está mal. Un servidor nunca debe rescatar Exception y no hacer nada más que iniciar sesión. Eso lo hará invencible excepto por kill -9.
Juan
8
Sus ejemplos en la nota 3 no son equivalentes, ensurese ejecutará independientemente de si se produjo una excepción o no, mientras rescueque solo se ejecutará si se generó una excepción.
Andrew Marshall
1
No son / exactamente / equivalentes, pero no puedo entender cómo expresar sucintamente la equivalencia de una manera que no sea fea.
Michael Slade
3
Simplemente agregue otra llamada critical_cleanup después del bloque de inicio / rescate en el primer ejemplo. No estoy de acuerdo con el código más elegante, pero obviamente el segundo ejemplo es la forma elegante de hacerlo, por lo que una pequeña falta de elegancia es solo parte del ejemplo.
gtd
3
"Ni siquiera ejecutes eso para ver si funciona". Parece un mal consejo para la codificación ... Por el contrario, le aconsejaría que lo ejecute, que lo vea fallar y que comprenda por sí mismo cómo hacerlo, en lugar de creer ciegamente en otra persona. Gran respuesta de todos modos :)
huelbois
69

TL; DR

No lo hagas rescue Exception => e(y no vuelvas a plantear la excepción), o podrías salirte de un puente.


Digamos que estás en un auto (corriendo Ruby). Recientemente instaló un nuevo volante con el sistema de actualización por aire (que utiliza eval), pero no sabía que uno de los programadores había estropeado la sintaxis.

Estás en un puente y te das cuenta de que vas un poco hacia la barandilla, así que giras a la izquierda.

def turn_left
  self.turn left:
end

¡Uy! Probablemente no sea Good ™, afortunadamente, Ruby plantea a SyntaxError.

El auto debe detenerse de inmediato, ¿verdad?

No

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

bip bip

Advertencia: Excepción de error de sintaxis capturada.

Información: Error registrado - Proceso continuo.

Se nota que algo está mal, y que pisar el frenos de emergencia ( ^C: Interrupt)

bip bip

Advertencia: Excepción de interrupción atrapada.

Información: Error registrado - Proceso continuo.

Sí, eso no ayudó mucho. Estás bastante cerca del riel, por lo que pones el auto en estacionamiento ( killing:) SignalException.

bip bip

Advertencia: Excepción de excepción de señal atrapada.

Información: Error registrado - Proceso continuo.

En el último segundo, sacas las llaves (kill -9 ), y el automóvil se detiene, se estrella contra el volante (el airbag no puede inflarse porque no detuvo el programa con gracia, lo cerró) y la computadora en la parte de atrás de su automóvil se estrella contra el asiento frente a él. Una lata medio llena de Coca-Cola se derrama sobre los papeles. Los comestibles en la parte posterior están triturados, y la mayoría están cubiertos de yema de huevo y leche. El auto necesita reparaciones y limpieza serias. (Pérdida de datos)

Esperemos que tenga seguro (copias de seguridad). Ah, sí, debido a que el airbag no se infló, probablemente estés herido (despedido, etc.).


¡Pero espera! Haymásrazones por las que es posible que desee usar rescue Exception => e!

Digamos que usted es ese automóvil y desea asegurarse de que el airbag se infla si el automóvil excede su impulso de frenado seguro.

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

Aquí está la excepción a la regla: puede atrapar Exception solo si vuelve a aumentar la excepción . Entonces, una mejor regla es nunca tragar Exception, y siempre volver a plantear el error.

Pero agregar rescate es fácil de olvidar en un lenguaje como Ruby, y poner una declaración de rescate justo antes de volver a plantear un problema se siente un poco no SECO. Y no quieres olvidar la raisedeclaración. Y si lo haces, buena suerte tratando de encontrar ese error.

Afortunadamente, Ruby es increíble, solo puedes usar la ensurepalabra clave, lo que asegura que el código se ejecute. La ensurepalabra clave ejecutará el código pase lo que pase: si se lanza una excepción, si no es así, la única excepción es si el mundo termina (u otros eventos poco probables).

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

¡Auge! Y ese código debería ejecutarse de todos modos. La única razón por la que debe usar rescue Exception => ees si necesita acceso a la excepción, o si solo desea que el código se ejecute en una excepción. Y recuerde volver a plantear el error. Cada vez.

Nota: Como señaló @Niall, asegúrese de que siempre se ejecute. Esto es bueno porque a veces su programa puede mentirle y no lanzar excepciones, incluso cuando ocurren problemas. Con tareas críticas, como inflar bolsas de aire, debe asegurarse de que suceda sin importar lo que pase. Debido a esto, es una buena idea verificar cada vez que el automóvil se detiene, si se produce una excepción o no. Aunque inflar airbags es una tarea poco común en la mayoría de los contextos de programación, esto es bastante común en la mayoría de las tareas de limpieza.

Ben Aubin
fuente
12
Jajajaja Esta es una respuesta genial. Estoy sorprendido de que nadie haya comentado. Da un escenario claro que hace que todo sea realmente comprensible. ¡Salud! :-)
James Milani
@JamesMilani ¡Gracias!
Ben Aubin
3
+ 💯 para esta respuesta. ¡Ojalá pudiera votar más de una vez! 😂
engineerDave
1
¡Disfruté tu respuesta!
Atul Vaibhav
3
Esta respuesta llegó 4 años después de la respuesta aceptada perfectamente comprensible y correcta, y la volvió a explicar con un escenario absurdo diseñado más para ser divertido que realista. Lamento ser un buzzkill, pero esto no es reddit: es más importante que las respuestas sean breves y correctas que divertidas. Además, la parte sobre ensurecomo alternativa rescue Exceptiones engañosa: el ejemplo implica que son equivalentes, pero como se dijo ensuresucederá si hay una Excepción o no, por lo que ahora sus bolsas de aire se inflarán porque superó las 5 mph, aunque nada salió mal.
Niall
47

Porque esto captura todas las excepciones. Es poco probable que su programa pueda recuperarse de cualquiera de ellos.

Debe manejar solo las excepciones de las que sabe cómo recuperarse. Si no anticipa un cierto tipo de excepción, no lo maneje, bloquee ruidosamente (escriba los detalles en el registro), luego diagnostique los registros y corrija el código.

Tragar excepciones es malo, no hagas esto.

Sergio Tulentsev
fuente
10

Ese es un caso específico de la regla de que no debe detectar ninguna excepción que no sepa cómo manejar. Si no sabe cómo manejarlo, siempre es mejor dejar que otra parte del sistema lo atrape y lo maneje.

Russell Borogove
fuente
0

Acabo de leer una gran publicación de blog al respecto en honeybadger.io :

Ruby's Exception vs StandardError: ¿Cuál es la diferencia?

¿Por qué no deberías rescatar a Exception?

El problema con el rescate de Exception es que en realidad rescata cada excepción que hereda de Exception. Lo cual es ... ¡todos ellos!

Eso es un problema porque hay algunas excepciones que Ruby usa internamente. No tienen nada que ver con tu aplicación, y tragarlas hará que sucedan cosas malas.

Estos son algunos de los grandes:

  • SignalException :: Interrupt - Si rescatas esto, no puedes salir de tu aplicación presionando control-c.

  • ScriptError :: SyntaxError - La ingestión de errores de sintaxis significa que cosas como put ("Olvidé algo" fallarán en silencio.

  • NoMemoryError - ¿Quieres saber qué sucede cuando tu programa sigue ejecutándose después de que usa toda la RAM? Yo tampoco.

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

Supongo que realmente no quieres tragarte ninguna de estas excepciones a nivel de sistema. Solo desea detectar todos los errores de nivel de aplicación. Las excepciones causaron SU código.

Afortunadamente, hay una manera fácil de hacerlo.

calebkm
fuente