Propagación de excepciones: ¿cuándo debo detectar excepciones?

44

El Método A llama a un Método B que a su vez llama al Método C.

No hay manejo de excepciones en MethodB o MethodC. Pero hay un manejo de excepciones en el Método A.

En MethodC ocurre una excepción.

Ahora, esa excepción está apareciendo en el Método A, que lo maneja adecuadamente.

¿Qué hay de malo en esto?

En mi opinión, en algún momento una persona que llama ejecutará MethodB o MethodC, y cuando se produzcan excepciones en esos métodos, lo que se obtendrá al manejar excepciones dentro de esos métodos, que esencialmente es solo un bloque try / catch / finally en lugar de simplemente dejarlo ellos burbujean hasta el llamado?

La declaración o consenso sobre el manejo de excepciones es arrojar cuando la ejecución no puede continuar debido a eso, una excepción. Lo entiendo. Pero, ¿por qué no atrapar la excepción más arriba en la cadena en lugar de tener bloques try / catch hasta el final?

Lo entiendo cuando necesitas liberar recursos. Esa es una cuestión completamente diferente.

Daniel Frost
fuente
46
¿Por qué crees que el consenso es tener una cadena de transferencia a través de las capturas?
Caleth
Con un buen IDE y un estilo de codificación adecuado, puede saber que puede producirse una excepción cuando se llama a un método. Manejarlo o permitir que se propague es decisión de la persona que llama. No veo ningún problema con esto.
Hieu Le
14
Si un método no puede manejar la excepción, y simplemente lo está volviendo a lanzar, diría que es un olor a código. Si un método no puede manejar la excepción y no necesita hacer nada más cuando se lanza una excepción, entonces no hay necesidad de un try-catchbloqueo en absoluto.
Greg Burghardt
77
"¿Qué tiene de malo esto?" : nada
Ewan
55
La captura de paso (que no incluye excepciones en diferentes tipos ni nada por el estilo) anula el propósito de las excepciones. El lanzamiento de excepciones es un mecanismo complejo, y fue construido intencionalmente. Si las capturas de paso fueron el caso de uso previsto, entonces todo lo que necesitaría es implementar un Result<T>tipo (un tipo que almacene un resultado de un cálculo o un error) y devolverlo de sus funciones de lanzamiento. Propagar un error en la pila implicaría leer cada valor de retorno, verificar si es un error y devolver un error si es así.
Alexander

Respuestas:

139

Como principio general, no capture excepciones a menos que sepa qué hacer con ellas. Si MethodC arroja una excepción, pero MethodB no tiene una forma útil de manejarla, entonces debería permitir que la excepción se propague hasta MethodA.

Las únicas razones por las que un método debe tener un mecanismo de captura y relanzamiento son:

  • Desea convertir una excepción a otra diferente que sea más significativa para la persona que llama arriba.
  • Desea agregar información adicional a la excepción.
  • Necesita una cláusula catch para limpiar los recursos que se filtrarían sin una.

De lo contrario, detectar excepciones en el nivel incorrecto tiende a generar un código que falla silenciosamente sin proporcionar comentarios útiles al código de llamada (y, en última instancia, al usuario del software). La alternativa de atrapar una excepción y luego volver a lanzarla inmediatamente no tiene sentido.

Simon B
fuente
28
@GregBurghardt si su idioma tiene algo parecido try ... finally ..., entonces
úselo
19
"detectar una excepción y luego volver a lanzarla inmediatamente no tiene sentido" Dependiendo del idioma y de cómo lo haga, puede ser activamente perjudicial para la base de código. A menudo, las personas que intentan esto eliminan mucha información sobre la excepción, como el stacktrace original. He tratado con el código donde la persona que llama recibe una excepción que es completamente engañosa en cuanto a lo que sucedió y dónde.
JimmyJames
77
"No atrape excepciones a menos que sepa qué hacer con ellas". Eso suena razonable a primera vista, pero causa problemas más adelante. Lo que está haciendo aquí es filtrar detalles de implementación a sus llamantes. Imagine que está utilizando un ORM particular en su implementación para cargar datos. Si no detecta las excepciones específicas de ese ORM, pero simplemente deja que broten, no puede reemplazar su capa de datos sin romper la compatibilidad con los usuarios existentes. Ese es uno de los casos más obvios, pero puede volverse bastante insidioso y es difícil de detectar.
Voo
11
@Voo En su ejemplo que no sabe qué hacer con él. Envuélvala en una excepción documentada específica de su código, por ejemplo, LoadDataExceptione incluya los detalles de la excepción original de acuerdo con las características de su idioma, de modo que los futuros encargados puedan ver la causa raíz sin tener que adjuntar un depurador y descubrir cómo reproducir el problema.
Colin Young
14
@Voo Parece que se ha perdido la razón "Quiere convertir una excepción a otra diferente que sea más significativa para la persona que llama arriba" para los escenarios de captura / relanzamiento.
jpmc26
21

¿Qué hay de malo en esto?

Absolutamente nada.

Ahora, esa excepción está apareciendo en el Método A, que lo maneja adecuadamente.

"lo maneja adecuadamente" es la parte importante. Ese es el quid de la gestión de excepciones estructuradas.

Si su código puede hacer algo "útil" con una excepción, hágalo. Si no, entonces déjalo bien.

. . . por qué no atrapar la excepción más arriba en la cadena en lugar de tener bloques try / catch hasta el final.

Eso es exactamente lo que deberías estar haciendo. Si está leyendo el código que tiene manejadores / lanzadores "completamente", entonces [probablemente] está leyendo un código bastante pobre.

Lamentablemente, algunos desarrolladores simplemente ven los bloques de captura como código de "placa de caldera" que arrojan (sin juego de palabras) a cada método que escriben, a menudo porque realmente no "obtienen" el manejo de excepciones y piensan que tienen que agregar algo que las excepciones no "escapan" y matan su programa.

Parte de la dificultad aquí es que, la mayoría de las veces, este problema ni siquiera se notará, porque las excepciones no se lanzan todo el tiempo, pero cuando lo hacen , el programa va a perder mucho tiempo y Esfuerzo gradualmente para eliminar la pila de llamadas para llegar a un lugar que realmente hace algo útil con la excepción.

Phill W.
fuente
77
Lo que es aún peor es cuando la aplicación detecta la excepción y luego la registra (donde es de esperar que no se quede allí para siempre) e intenta continuar como siempre, incluso cuando realmente no puede.
Solomon Ucko
1
@SolomonUcko: Bueno, depende. Si, por ejemplo, está escribiendo un servidor RPC simple, y una excepción no controlada se propaga hasta el bucle de eventos principal, su única opción razonable es registrarlo, enviar un error RPC al par remoto y reanudar el procesamiento de eventos. La otra alternativa es matar todo el programa, lo que volverá loco a su SRE cuando el servidor muera en producción.
Kevin
@Kevin En ese caso, debería haber un único catchen el nivel más alto posible que registre el error y devuelva una respuesta de error. No catchbloques esparcidos por todas partes. Si no tiene ganas de enumerar todas las posibles excepciones marcadas (en lenguajes como Java), simplemente envuélvalas en RuntimeExceptionlugar de iniciar sesión allí, intentar continuar y encontrarse con más errores o incluso vulnerabilidades.
Solomon Ucko
8

Debe marcar la diferencia entre las bibliotecas y las aplicaciones.

Las bibliotecas pueden lanzar excepciones no capturadas libremente

Cuando diseña una biblioteca, en algún momento debe pensar qué puede salir mal. Los parámetros pueden estar en el rango incorrecto o null, los recursos externos pueden no estar disponibles, etc.

Su biblioteca con mayor frecuencia no tendrá una manera de tratarlos de manera sensata . La única solución sensata es lanzar una Excepción apropiada y dejar que el desarrollador de la Aplicación se encargue de ella.

Las aplicaciones siempre deberían, en algún momento, detectar excepciones

Cuando se detecta una excepción, me gusta clasificarlos como errores o errores fatales . Un error regular significa que una sola operación dentro de mi aplicación falló. Por ejemplo, un documento abierto no se pudo guardar porque el destino no se podía escribir. Lo único que debe hacer la aplicación es informar al usuario que la operación no se pudo completar con éxito, proporcionar información legible para el problema y luego dejar que el usuario decida qué hacer a continuación.

Un error fatal es un error del que la lógica principal de la aplicación no puede recuperarse. Por ejemplo, si el controlador del dispositivo gráfico se bloquea en un videojuego, no hay forma de que la aplicación informe "con gracia" al usuario. En este caso, se debe escribir un archivo de registro y, si es posible, se debe informar al usuario de una forma u otra.

Incluso en un caso tan grave, la Aplicación debe manejar esta Excepción de manera significativa. Esto puede incluir escribir un archivo de registro, enviar un informe de bloqueo, etc. No hay ninguna razón para que la aplicación no responda a la excepción de alguna manera.

MechMK1
fuente
De hecho, si tiene una biblioteca para algo como operaciones de escritura en disco o, por ejemplo, otra manipulación de hardware, puede terminar en todo tipo de eventos inesperados. ¿Qué sucede si se extrae un disco duro durante la escritura? ¿Qué unidad de CD se acorta mientras lee? Eso está fuera de su control y, aunque puede hacer algo (por ejemplo, pretender que fue exitoso), a menudo es una buena práctica lanzar una excepción al usuario de la biblioteca y dejar que decida. Tal vez se HDDPluggedOutDuringWritingExceptionpueda resolver un problema y no sea fatal para la aplicación. El programa puede decidir qué hacer con eso.
VLAZ
1
@VLAZ Lo que es fatal versus no fatal es algo que la aplicación debe decidir. La biblioteca debería contar lo que pasó. La aplicación tiene que decidir cómo reaccionar ante ella.
MechMK1
0

Lo que está mal con el patrón que describe es que el método A no tendrá forma de distinguir entre tres escenarios:

  1. El Método B falló de manera anticipada.

  2. El método C falló de una manera no prevista por el método B, pero mientras el método B realizaba una operación que podría abandonarse de manera segura.

  3. El método C falló de una manera no prevista por el método B, pero mientras el método B realizaba una operación que colocaba las cosas en un estado incoherente que supuestamente sería temporal, que B no pudo limpiar debido a la falla de C.

La única forma en que el método A podrá distinguir esos escenarios será si la excepción lanzada desde B incluye información suficiente para ese propósito, o si el desbobinado de la pila para el método B hace que el objeto quede en un estado explícitamente invalidado . Desafortunadamente, la mayoría de los marcos de excepción hacen que ambos patrones sean incómodos, lo que obliga a los programadores a tomar decisiones de diseño de "menor maldad".

Super gato
fuente
2
Los escenarios 2 y 3 son errores en el método B. El método A no debería intentar solucionarlos.
Sjoerd
@Sjoerd: ¿Cómo se supone que el método B anticipa todas las formas en que el método C podría fallar?
supercat
Mediante patrones bien conocidos como realizar todas las operaciones que podrían arrojar variables temporales, luego con operaciones que no pueden arrojar, por ejemplo, intercambiar, intercambiar lo antiguo con el nuevo estado. Otro patrón es la definición de operaciones que se pueden repetir de forma segura, por lo que puede volver a intentar la operación sin temor a equivocarse. Hay libros completos sobre cómo escribir 'código seguro de excepción', por lo que no puedo contarte todo aquí.
Sjoerd
Este es un buen punto para no usar excepciones (lo cual sería una excelente decisión en mi humilde opinión). Pero supongo que en realidad no responde a la pregunta, ya que el OP parece tener la intención de usar excepciones en primer lugar, y solo pregunta dónde debería estar la captura.
cmaster
@Sjoerd El método B se vuelve mucho más fácil de razonar si el lenguaje prohíbe las excepciones. Porque en ese caso, en realidad ve todas las rutas de flujo de control a través de B, y no tiene que adivinar qué operadores podrían estar sobrecargados (C ++) para evitar el escenario 3. Estamos pagando mucho en términos de código claridad y seguridad para ser flojo al devolver errores "simplemente" lanzando excepciones. Porque, al final, el manejo de errores es una parte vital del código.
cmaster