Mejores prácticas para la gestión de excepciones en Java o C # [cerrado]

117

Estoy atascado decidiendo cómo manejar las excepciones en mi aplicación.

Mucho si mis problemas con las excepciones provienen de 1) acceder a los datos a través de un servicio remoto o 2) deserializar un objeto JSON. Desafortunadamente, no puedo garantizar el éxito de ninguna de estas tareas (cortar la conexión de red, objeto JSON mal formado que está fuera de mi control).

Como resultado, si encuentro una excepción, simplemente la atrapo dentro de la función y devuelvo FALSE a la persona que llama. Mi lógica es que todo lo que realmente le importa a la persona que llama es si la tarea fue exitosa, no por qué no fue exitosa.

Aquí hay un código de muestra (en JAVA) de un método típico)

public boolean doSomething(Object p_somthingToDoOn)
{
    boolean result = false;

    try{
        // if dirty object then clean
        doactualStuffOnObject(p_jsonObject);

        //assume success (no exception thrown)
        result = true;
    }
    catch(Exception Ex)
    {
        //don't care about exceptions
        Ex.printStackTrace();
    }
    return result;
}

Creo que este enfoque está bien, pero tengo mucha curiosidad por saber cuáles son las mejores prácticas para administrar excepciones (¿debería realmente hacer burbujear una excepción en una pila de llamadas?).

En resumen de preguntas clave:

  1. ¿Está bien simplemente detectar las excepciones pero no hacerlas burbujear o notificar formalmente al sistema (ya sea a través de un registro o una notificación al usuario)?
  2. ¿Qué mejores prácticas existen para las excepciones que no dan como resultado que todo requiera un bloque try / catch?

Seguimiento / Editar

Gracias por todos los comentarios, encontré algunas fuentes excelentes sobre la gestión de excepciones en línea:

Parece que la gestión de excepciones es una de esas cosas que varían según el contexto. Pero lo más importante es que uno debe ser coherente en la forma en que administran las excepciones dentro de un sistema.

Además, tenga cuidado con la descomposición del código a través de intentos / capturas excesivos o no otorgando una excepción a su respeto (una excepción es una advertencia al sistema, ¿qué más se debe advertir?).

Además, este es un bonito comentario de elección de m3rLinEz .

Tiendo a estar de acuerdo con Anders Hejlsberg y usted en que a la mayoría de las personas que llaman solo les importa si la operación es exitosa o no.

A partir de este comentario, surgen algunas preguntas en las que pensar cuando se trata de excepciones:

  • ¿Cuál es el punto de lanzar esta excepción?
  • ¿Cómo tiene sentido manejarlo?
  • ¿A la persona que llama realmente le importa la excepción o simplemente le importa si la llamada fue exitosa?
  • ¿Es correcto forzar a una persona que llama a administrar una posible excepción?
  • ¿Estás siendo respetuoso con los ídolos del idioma?
    • ¿Realmente necesitas devolver un indicador de éxito como booleano? Devolver booleano (o un int) es más una mentalidad de C que una de Java (en Java, solo manejaría la excepción).
    • Siga las construcciones de gestión de errores asociadas con el idioma :)!
AtariPete
fuente
Aunque el artículo de la comunidad de Oracle está en Java, es un consejo genérico para una clase de idiomas bastante amplia. Buen artículo, justo lo que estaba buscando.
Bunny Rabbit

Respuestas:

61

Me parece extraño que desee detectar excepciones y convertirlas en códigos de error. ¿Por qué cree que la persona que llama preferiría los códigos de error a las excepciones cuando esta última es la predeterminada tanto en Java como en C #?

En cuanto a tus preguntas:

  1. Solo debe detectar las excepciones que realmente pueda manejar. En la mayoría de los casos, solo detectar excepciones no es lo correcto. Hay algunas excepciones (por ejemplo, registro y clasificación de excepciones entre subprocesos), pero incluso para esos casos, generalmente debería volver a generar las excepciones.
  2. Definitivamente no debería tener muchas declaraciones try / catch en su código. Nuevamente, la idea es detectar solo las excepciones que pueda manejar. Puede incluir un controlador de excepciones superior para convertir las excepciones no controladas en algo útil para el usuario final, pero de lo contrario, no debe intentar capturar todas y cada una de las excepciones en todos los lugares posibles.
Brian Rasmussen
fuente
En cuanto a por qué alguien querría códigos de error en lugar de excepciones ... Siempre pensé que era extraño que HTTP todavía usara códigos de error, a pesar de que mi aplicación está generando una excepción. ¿Por qué HTTP no me permite pasar la excepción tal como está?
Trejkaz
lo mejor que puede hacer es envolver sus códigos de error en una mónada
lindenrovio
@Trejkaz No desea devolver el detalle de la excepción a los usuarios, porque es un riesgo de seguridad. Es por eso que los servidores HTML devuelven códigos de error. También es un problema de localización devolver un mensaje de error, y probablemente hace que el HTML sea más grande y más lento para regresar. Todas estas son las razones por las que creo que los servidores HTML devuelven códigos de error.
Didier A.
Creo que es mejor decir: "Solo debe suprimir las excepciones que realmente pueda manejar".
Didier A.
@didibus ¿Por qué cree que las preocupaciones de seguridad deberían crear un inconveniente en el desarrollo? Nunca dije nada sobre producción. En cuanto a la localización de mensajes de error, todo el sitio web ya tiene ese problema y la gente parece estar haciéndolo.
Trejkaz
25

Depende de la aplicación y la situación. Si está construyendo un componente de biblioteca, debe incluir excepciones, aunque deben ajustarse para que sean contextuales con su componente. Por ejemplo, si está creando una base de datos Xml y digamos que está utilizando el sistema de archivos para almacenar sus datos, y está utilizando permisos del sistema de archivos para proteger los datos. No querrá hacer brotar una excepción FileIOAccessDenied ya que eso filtra su implementación. En su lugar, envolvería la excepción y lanzaría un error de AccessDenied. Esto es especialmente cierto si distribuye el componente a terceros.

En cuanto a si está bien aceptar excepciones. Eso depende de tu sistema. Si su aplicación puede manejar los casos de falla y no hay ningún beneficio en notificar al usuario por qué falló, continúe, aunque le recomiendo que registre la falla. Siempre me ha resultado frustrante ser llamado para ayudar a solucionar un problema y descubrir que se estaban tragando la excepción (o reemplazándola y lanzando una nueva en su lugar sin establecer la excepción interna).

En general, utilizo las siguientes reglas:

  1. En mis componentes y bibliotecas, solo detecta una excepción si tengo la intención de manejarla o hacer algo basado en ella. O si quiero proporcionar información contextual adicional en una excepción.
  2. Utilizo una captura de prueba general en el punto de entrada de la aplicación o en el nivel más alto posible. Si llega una excepción, simplemente la registro y dejo que falle. Idealmente, las excepciones nunca deberían llegar aquí.

Encuentro que el siguiente código es un olor:

try
{
    //do something
}
catch(Exception)
{
   throw;
}

Un código como este no sirve de nada y no debe incluirse.

JoshBerke
fuente
@Josh, un buen punto sobre tragar excepciones, pero creo que hay pocos casos en los que es aceptable simplemente tragar excepciones. En el último proyecto, se ordenó el fragmento de código, apestaba. Registrarlos todos empeoró las cosas. Mi consejo, si no puede manejar la excepción, trate de no tragarla.
smaclell
Agregado como dije, todo depende de la aplicación y el contexto específico. Hay momentos en los que trago excepciones, aunque es raro, y no puedo recordar la última vez que lo hice ;-) Probablemente fue cuando estaba escribiendo mi propio registrador, y la escritura en el registro y el registro secundario fallaron.
JoshBerke
El código tiene un punto: puede establecer un punto de interrupción en el "lanzamiento".
Rauhotz
3
Punto débil, puede decirle a VS que se rompa en cualquier excepción lanzada o puede reducirla y elegir una excepción específica. En VS2008 hay un elemento de menú en depuración (deberá personalizar las barras de herramientas para encontrarlo) Excepciones
llamadas
El ejemplo del "olor a código" tiene un efecto secundario definido, incluso en esa forma simple. Si // do somethingincluye try/finallybloques alrededor del punto que lanza, los finallybloques se ejecutarán antes que el catchbloque. Sin el try/catch, la excepción volará hasta la parte superior de la pila sin que finallyse ejecute ningún bloque. Esto permite que el controlador de nivel superior decida si ejecutar o no los finallybloques.
Daniel Earwicker
9

Me gustaría recomendar otra buena fuente sobre el tema. Es una entrevista con los inventores de C # y Java, Anders Hejlsberg y James Gosling respectivamente, sobre el tema de la excepción marcada de Java.

Fallos y excepciones

También hay excelentes recursos al final de la página.

Tiendo a estar de acuerdo con Anders Hejlsberg y usted en que a la mayoría de las personas que llaman solo les importa si la operación es exitosa o no.

Bill Venners : Mencionaste problemas de escalabilidad y versiones con respecto a las excepciones marcadas. ¿Podría aclarar qué quiere decir con esos dos temas?

Anders Hejlsberg : Comencemos con el control de versiones, porque los problemas son bastante fáciles de ver allí. Digamos que creo un método foo que declara que arroja las excepciones A, B y C.En la versión dos de foo, quiero agregar un montón de características, y ahora foo podría lanzar la excepción D. Es un cambio importante para mí. agregue D a la cláusula throws de ese método, porque es casi seguro que el llamador existente de ese método no manejará esa excepción.

Agregar una nueva excepción a una cláusula throws en una nueva versión rompe el código del cliente. Es como agregar un método a una interfaz. Después de publicar una interfaz, es inmutable a todos los efectos prácticos, porque cualquier implementación de la misma puede tener los métodos que desea agregar en la próxima versión. Entonces tienes que crear una nueva interfaz en su lugar. De manera similar, con las excepciones, tendría que crear un método completamente nuevo llamado foo2 que arroje más excepciones, o tendría que detectar la excepción D en el nuevo foo y transformar la D en una A, B o C.

Bill Venners : ¿Pero no está rompiendo su código en ese caso de todos modos, incluso en un idioma sin excepciones marcadas? Si la nueva versión de foo va a lanzar una nueva excepción que los clientes deberían pensar en manejar, ¿no se rompe su código solo por el hecho de que no esperaban esa excepción cuando escribieron el código?

Anders Hejlsberg : No, porque en muchos casos, a la gente no le importa. No van a manejar ninguna de estas excepciones. Hay un controlador de excepciones de nivel inferior alrededor de su bucle de mensajes. Ese controlador solo va a abrir un diálogo que dice qué salió mal y continuar. Los programadores protegen su código escribiendo try finalmente en todas partes, por lo que retrocederán correctamente si ocurre una excepción, pero en realidad no están interesados ​​en manejar las excepciones.

La cláusula throws, al menos la forma en que se implementa en Java, no necesariamente te obliga a manejar las excepciones, pero si no las manejas, te obliga a reconocer con precisión qué excepciones pueden pasar. Requiere que usted capture las excepciones declaradas o las ponga en su propia cláusula throws. Para evitar este requisito, la gente hace cosas ridículas. Por ejemplo, decoran cada método con "throws Exception". Eso simplemente anula por completo la función, y acaba de hacer que el programador escriba más basura. Eso no ayuda a nadie.

EDITAR: Se agregaron más detalles sobre la conversión.

Gant
fuente
¡Gracias por esto! ¡Actualicé mi pregunta con información sobre tu respuesta!
AtariPete
Parece que el Sr. Hejlsberg está justificando el manejo de excepciones de Pokémon. Uno de los grandes problemas con el diseño de excepciones en Java y C # es que demasiada información está codificada en el tipo de excepción, mientras que la información debe almacenarse en instancias de excepción que no están disponibles de manera consistente. Las excepciones deben propagarse por la pila de llamadas hasta que se hayan resuelto todas las condiciones anormales representadas por ellas; Desafortunadamente, el tipo de excepción, incluso si se reconoce, no indica si una situación está resuelta. Si no se reconoce una excepción ...
supercat
... la situación es aún peor. Si el código llama FetchDatay arroja una excepción de un tipo inesperado, no tiene forma de saber si esa excepción simplemente significa que los datos no están disponibles (en cuyo caso la capacidad del código para funcionar sin ellos lo "resolvería"), o si significa que la CPU está en llamas y el sistema debe realizar un "apagado de seguridad" en la primera oportunidad. Parece que el Sr. Hejlsberg está sugiriendo que el código debería asumir lo primero; quizás esa sea la mejor estrategia posible dadas las jerarquías de excepciones existentes, pero no obstante, parece asqueroso.
supercat
Estoy de acuerdo en que throws Exceptin es ridículo, porque casi cualquier cosa puede generar una excepción, debes asumir que esto puede suceder. Pero cuando especifica excepciones como lanzamientos A, B, C, es solo una anotación, para mí, debería usarse como un bloque de comentarios. Es como decir, oye cliente de mi función, aquí hay un consejo, tal vez quieras lidiar con A, B, C, porque es probable que estos errores ocurran cuando me utilices. Si se agrega D en el futuro, que no es un gran problema si no está siendo tratado, pero es como la adición de nueva documentación, Ah, por cierto, ahora también sería útil para atrapar D.
Didier A.
8

Las excepciones marcadas son un tema controvertido en general, y en Java en particular (más adelante intentaré encontrar algunos ejemplos para quienes están a favor y en contra).

Como reglas generales, el manejo de excepciones debe basarse en estas pautas, sin ningún orden en particular:

  • En aras de la facilidad de mantenimiento, siempre registre las excepciones para que cuando comience a ver errores, el registro lo ayude a indicarle el lugar donde probablemente comenzó su error. Nunca te vayas printStackTrace()o algo así, es probable que uno de tus usuarios obtenga uno de esos rastros de pila eventualmente y no tenga exactamente ningún conocimiento sobre qué hacer con él.
  • Detecte las excepciones que pueda manejar, y solo aquellas, y manejelas , no las arroje a la pila.
  • Capture siempre una clase de excepción específica y, por lo general, nunca debe capturar el tipo Exception, es muy probable que se trague excepciones importantes.
  • ¡Nunca (nunca) atrape Errors !! , lo que significa: Nunca capturas Throwables ya que Errors son subclases de este último. Errors son problemas que probablemente nunca podrá manejar (por ejemplo OutOfMemory, u otros problemas de JVM)

Con respecto a su caso específico, asegúrese de que cualquier cliente que llame a su método reciba el valor de retorno adecuado. Si algo falla, un método de retorno booleano podría devolver falso, pero asegúrese de que los lugares a los que llama a ese método puedan manejar eso.

Yuval Adam
fuente
Las excepciones marcadas no son un problema en C # porque no las tiene.
cletus
3
En mi humilde opinión, a veces es bueno detectar errores: recuerdo una aplicación Java muy intensiva en memoria que escribí. Cogí el OutOfMemory-Ex, y le mostré un mensaje al usuario que no tenía memoria, que debería salir de otros programas y le dije cómo iniciar el jvm con más espacio de pila asignado. Supongo que ayudó.
Lena Schimmel
5

Solo debe detectar las excepciones con las que pueda lidiar. Por ejemplo, si está leyendo en una red y la conexión se agota y obtiene una excepción, puede intentarlo de nuevo. Sin embargo, si está leyendo en una red y obtiene una excepción IndexOutOfBounds, realmente no puede manejar eso porque no sabe (bueno, en este caso no) sabe qué lo causó. Si va a devolver falso, -1 o nulo, asegúrese de que sea para excepciones específicas. No quiero que una biblioteca que estoy usando devuelva un falso en una lectura de red cuando la excepción lanzada es que el montón no tiene memoria.

Malfist
fuente
3

Las excepciones son errores que no forman parte de la ejecución normal del programa. Dependiendo de lo que haga su programa y sus usos (es decir, un procesador de texto frente a un monitor cardíaco), querrá hacer cosas diferentes cuando encuentre una excepción. He trabajado con código que usa excepciones como parte de la ejecución normal y definitivamente es un olor a código.

Ex.

try
{
   sendMessage();

   if(message == success)
   {
       doStuff();
   }
   else if(message == failed)
   {
       throw;
   }
}
catch(Exception)
{
    logAndRecover();
}

Este código me hace vomitar. En mi opinión, no debe recuperarse de las excepciones a menos que sea un programa crítico. Si lanza excepciones, entonces están sucediendo cosas malas.

Holograham
fuente
2

Todo lo anterior parece razonable y, a menudo, su lugar de trabajo puede tener una política. En nuestro lugar hemos definido tipos de excepción: SystemException(sin marcar) y ApplicationException(marcada).

Hemos acordado que SystemExceptiones poco probable que los correos electrónicos sean recuperables y que se manejarán una vez en la parte superior. Para proporcionar más contexto, nuestros SystemExceptions se exteneded para indicar dónde se produjeron, por ejemplo RepositoryException, ServiceEception, etc.

ApplicationExceptions podría tener un significado comercial como InsufficientFundsExceptiony debe manejarse mediante el código del cliente.

Sin un ejemplo concreto, es difícil comentar sobre su implementación, pero nunca usaría códigos de retorno, son un problema de mantenimiento. Puede tragarse una excepción, pero debe decidir por qué y siempre registrar el evento y el seguimiento de la pila. Por último, como su método no tiene otro procesamiento, es bastante redundante (¿excepto para la encapsulación?), ¡Por lo que doactualStuffOnObject(p_jsonObject);podría devolver un booleano!


fuente
1

Después de pensar un poco y mirar su código, me parece que simplemente está volviendo a generar la excepción como booleana. Puede dejar que el método pase esta excepción (ni siquiera tiene que detectarla) y tratarla en la persona que llama, ya que ese es el lugar donde importa. Si la excepción hará que la persona que llama vuelva a intentar esta función, la persona que llama debe ser la que reciba la excepción.

A veces puede suceder que la excepción que está encontrando no tenga sentido para la persona que llama (es decir, es una excepción de red), en cuyo caso debe incluirla en una excepción específica de dominio.

Si, por otro lado, la excepción indica un error irrecuperable en su programa (es decir, el resultado final de esta excepción será la terminación del programa), personalmente me gusta hacer eso explícito al detectarlo y lanzar una excepción de tiempo de ejecución.

wds
fuente
1

Si va a utilizar el patrón de código en su ejemplo, llámelo TryDoSomething y capture solo excepciones específicas.

También considere usar un filtro de excepciones al registrar excepciones con fines de diagnóstico. VB tiene soporte de idioma para filtros de excepción. El enlace al blog de Greggm tiene una implementación que se puede usar desde C #. Los filtros de excepción tienen mejores propiedades de depuración que la captura y el relanzamiento. Específicamente, puede registrar el problema en el filtro y dejar que la excepción continúe propagándose. Ese método permite adjuntar un depurador JIT (Just in Time) para tener la pila original completa. Un nuevo lanzamiento corta la pila en el punto en que se volvió a lanzar.

Los casos en los que TryXXXX tiene sentido son cuando está ajustando una función de terceros que arroja casos que no son realmente excepcionales o que son simples y difíciles de probar sin llamar a la función. Un ejemplo sería algo como:

// throws NumberNotHexidecimalException
int ParseHexidecimal(string numberToParse); 

bool TryParseHexidecimal(string numberToParse, out int parsedInt)
{
     try
     {
         parsedInt = ParseHexidecimal(numberToParse);
         return true;
     }
     catch(NumberNotHexidecimalException ex)
     {
         parsedInt = 0;
         return false;
     }
     catch(Exception ex)
     {
         // Implement the error policy for unexpected exceptions:
         // log a callstack, assert if a debugger is attached etc.
         LogRetailAssert(ex);
         // rethrow the exception
         // The downside is that a JIT debugger will have the next
         // line as the place that threw the exception, rather than
         // the original location further down the stack.
         throw;
         // A better practice is to use an exception filter here.
         // see the link to Exception Filter Inject above
         // http://code.msdn.microsoft.com/ExceptionFilterInjct
     }
}

Si usa un patrón como TryXXX o no, es más una cuestión de estilo. La cuestión de captar todas las excepciones y tragarlas no es una cuestión de estilo. ¡Asegúrese de que se permita la propagación de excepciones inesperadas!

Steve Steiner
fuente
Me encanta el patrón TryXXX en .net.
JoshBerke
1

Sugiero tomar sus pistas de la biblioteca estándar para el idioma que está usando. No puedo hablar por C #, pero veamos Java.

Por ejemplo, java.lang.reflect.Array tiene un setmétodo estático :

static void set(Object array, int index, Object value);

La forma C sería

static int set(Object array, int index, Object value);

... siendo el valor de retorno un indicador de éxito. Pero ya no estás en el mundo C.

Una vez que acepte las excepciones, debería encontrar que hace que su código sea más simple y claro, al alejar su código de manejo de errores de su lógica central. Trate de tener muchas declaraciones en un solo trybloque.

Como han señalado otros, debe ser lo más específico posible en el tipo de excepción que detecta.

Delgado
fuente
este es un comentario muy válido, sea respetuoso con el lenguaje y cómo tradicionalmente maneja estos temas. No traiga una mentalidad C a un mundo Java.
AtariPete
0

Si va a detectar una excepción y devolver falso, debería ser una excepción muy específica. No estás haciendo eso, los estás capturando a todos y devolviendo falsos. Si obtengo una MyCarIsOnFireException, ¡quiero saberlo de inmediato! El resto de las excepciones puede que no me importe. Por lo tanto, debería tener una pila de manejadores de excepciones que digan "whoa whoa, algo está mal aquí" para algunas excepciones (volver a lanzar, o capturar y volver a lanzar una nueva excepción que explique mejor lo que sucedió) y simplemente devolver falso para otras.

Si este es un producto que va a lanzar, debería registrar esas excepciones en algún lugar, lo ayudará a ajustar las cosas en el futuro.

Editar: En cuanto a la cuestión de envolver todo en un intento / captura, creo que la respuesta es sí. Las excepciones deberían ser tan raras en su código que el código en el bloque de captura se ejecuta tan raramente que no afecta el rendimiento en absoluto. Una excepción debería ser un estado en el que su máquina de estado se rompió y no sabe qué hacer. Al menos, vuelva a generar una excepción que explique lo que estaba sucediendo en ese momento y que contenga la excepción detectada. "Excepción en el método doSomeStuff ()" no es muy útil para cualquiera que tenga que averiguar por qué se rompió mientras está de vacaciones (o en un nuevo trabajo).

jcollum
fuente
No olvide que la configuración de un bloque de excepción también cuesta ...
devstuff
0

Mi estrategia:

Si la función original devolvió vacío , la cambio para devolver bool . Si se produjo una excepción / error, devuelva falso , si todo estuvo bien, devuelva verdadero .

Si la función debe devolver algo, cuando se produzca una excepción / error, devuelva nulo ; de lo contrario, el artículo devuelto .

En lugar de bool, se podría devolver una cadena que contenga la descripción del error.

En todos los casos, antes de devolver algo, registre el error.

Tormenta de gérmenes
fuente
0

Algunas excelentes respuestas aquí. Me gustaría agregar que si termina con algo como lo que publicó, al menos imprima más que el seguimiento de la pila. Di lo que estabas haciendo en ese momento, y Ex.getMessage (), para darle al desarrollador una oportunidad de luchar.

dj_segfault
fuente
estoy totalmente de acuerdo. Solo hice Ex.printStackTrace (); como un ejemplo de algo que estaba haciendo en la captura (es decir, no volver a lanzar).
AtariPete
0

Los bloques try / catch forman un segundo conjunto de lógica incrustado sobre el primer conjunto (principal), como tal, son una excelente manera de eliminar códigos espaguetis ilegibles y difíciles de depurar.

Aún así, si se usan razonablemente, funcionan de maravilla en cuanto a legibilidad, pero debes seguir dos reglas simples:

  • úselos (con moderación) en el nivel bajo para detectar problemas de manejo de bibliotecas y transmitirlos de nuevo al flujo lógico principal. La mayor parte del manejo de errores que queremos debe provenir del propio código, como parte de los datos en sí. ¿Por qué establecer condiciones especiales si los datos que se devuelven no son especiales?

  • use un gran controlador en el nivel superior para administrar cualquiera o todas las condiciones extrañas que surgen en el código que no se detectan en un nivel bajo. Haga algo útil con los errores (registros, reinicios, recuperaciones, etc.).

Aparte de estos dos tipos de manejo de errores, todo el resto del código en el medio debe estar libre y libre de códigos de prueba / captura y objetos de error. De esa manera, funciona de manera simple y como se espera sin importar dónde lo use o qué haga con él.

Pablo.

Paul W Homer
fuente
0

Puede que llegue un poco tarde con la respuesta, pero el manejo de errores es algo que siempre podemos cambiar y evolucionar con el tiempo. Si quieres leer algo más sobre este tema, escribí un post en mi nuevo blog al respecto. http://taoofdevelopment.wordpress.com

Codificación feliz.

GRGodoi
fuente