Cómo escribir un buen mensaje de excepción

101

Actualmente estoy haciendo una revisión del código y una de las cosas que noto es la cantidad de excepciones en las que el mensaje de excepción parece reiterar dónde ocurrió la excepción. p.ej

throw new Exception("BulletListControl: CreateChildControls failed.");

Los tres elementos en este mensaje los puedo resolver del resto de la excepción. Sé la clase y el método del seguimiento de la pila y sé que falló (porque tengo una excepción).

Me hizo pensar en qué mensaje pongo en los mensajes de excepción. Primero creo una clase de excepción, si aún no existe, por la razón general (por ejemplo PropertyNotFoundException, el por qué ), y luego, cuando la lanzo, el mensaje indica qué salió mal (por ejemplo, "No se puede encontrar la propiedad 'IDontExist' en el Nodo 1234 "- el qué ). El donde está en el StackTrace. El cuándo puede terminar en el registro (si corresponde). El cómo es para que el desarrollador resuelva (y arregle)

¿Tienes algún otro consejo para lanzar excepciones? Específicamente con respecto a la creación de nuevos tipos y el mensaje de excepción.

Colin Mackay
fuente
44
¿Son para archivos de registro o para presentar al usuario?
Jon Hopkins el
55
Solo para depuración. Pueden terminar en un registro. No serían presentados al usuario. No soy fanático de presentar mensajes de excepción al usuario.
Colin Mackay

Respuestas:

72

Dirigiré mi respuesta más a lo que viene después de una excepción: para qué sirve y cómo debe comportarse el software, ¿qué deben hacer sus usuarios con la excepción? Una gran técnica que encontré al principio de mi carrera fue siempre informar problemas y errores en 3 partes: contexto, problema y solución. El uso de esta diciplina cambia enormemente el manejo de errores y hace que el software sea mucho mejor para que lo utilicen los operadores.

Aquí hay algunos ejemplos.

Context: Saving connection pooling configuration changes to disk.
Problem: Write permission denied on file '/xxx/yyy'.
Solution: Grant write permission to the file.

En este caso, el operador sabe exactamente qué hacer y a qué archivo debe verse afectado. También saben que los cambios de agrupación de conexiones no tomaron y deberían repetirse.

Context: Sending email to '[email protected]' regarding 'Blah'.
Problem: SMTP connection refused by server 'mail.xyz.com'.
Solution: Contact the mail server administrator to report a service problem.  The email will be sent later. You may want to tell '[email protected]' about this problem.

Escribo sistemas del lado del servidor y mis operadores son generalmente expertos en tecnología de primera línea. Escribiría los mensajes de manera diferente para el software de escritorio que tiene una audiencia diferente pero incluye la misma información.

Varias cosas maravillosas suceden si uno usa esta técnica. El desarrollador de software a menudo está mejor ubicado para saber cómo resolver los problemas en su propio código, por lo que codificar las soluciones de esta manera a medida que escribe el código es de gran beneficio para los usuarios finales que están en desventaja al encontrar soluciones, ya que a menudo les falta información sobre qué estaba haciendo exactamente el software. Cualquiera que haya leído un mensaje de error de Oracle sabrá a qué me refiero.

La segunda cosa maravillosa que viene a la mente es cuando te encuentras tratando de describir una solución en tu excepción y estás escribiendo "Marque X y si A entonces B más C". Esta es una señal muy clara y obvia de que su excepción se está verificando en el lugar incorrecto. Usted, el programador, tiene la capacidad de comparar cosas en el código, por lo que las declaraciones "si" deben ejecutarse en el código, ¿por qué involucrar al usuario en algo que pueda automatizarse? Lo más probable es que es de más profundo en el código y alguien ha hecho lo vago y arrojado IOException de cualquier número de métodos y atrapado posibles errores de todos ellos en un bloque de código de llamada que no se puede describir adecuadamente lo que salió mal, lo que la específicacontexto es y cómo solucionarlo. Esto lo alienta a escribir errores de grano más fino, atraparlos y manejarlos en el lugar correcto en su código para que pueda articular adecuadamente los pasos que debe seguir el operador.

En una compañía teníamos operadores de primer nivel que conocían muy bien el software y mantenían su propio "libro de ejecución" que aumentaba nuestros informes de errores y las soluciones sugeridas. Para reconocer esto, el software comenzó a incluir enlaces wiki al libro de carreras en excepciones, de modo que una explicación básica estaba disponible, así como enlaces a discusiones y observaciones más avanzadas por parte de los operadores a lo largo del tiempo.

Si ha tenido la diciplina para probar esta técnica, se vuelve mucho más obvio qué debe nombrar sus excepciones en el código al crear el suyo. NonRecoverableConfigurationReadFailedException se convierte en una abreviatura de lo que está a punto de describir más completamente al operador. Me gusta ser detallado y creo que será más fácil de interpretar para el próximo desarrollador que toque mi código.

Sir Wobin
fuente
1
+1 Este es un buen sistema. ¿Qué es más importante: estar seguro de que la información se transmite o usar palabras cortas?
Michael K
3
+1 para me gusta la solución de incluir, contexto, problema, solución
WebDev
1
Esta técnica es muy útil. Definitivamente voy a utilizarlo.
Kid Diamond
El contexto son datos innecesarios. Ya está presente en el seguimiento de la pila. Tener una solución es preferible pero no siempre es posible / útil. La mayoría de los problemas consiste en detener la aplicación o ignorar las operaciones pendientes y volver al ciclo de ejecución principal de la aplicación con la esperanza de que la próxima vez que tenga éxito ... El nombre de la clase de excepción debe ser tal que la solución sea ​​obvia, FileNotFoundo ConnectExceptionsabrá qué hacer))
gavenkoa
2
@ThomasFlinkow En su ejemplo, los seguimientos tendrán init (), execute () y cleanup () en el seguimiento de la pila. Con un buen esquema de nombres en la biblioteca y una API limpia / comprensible, no necesita explicaciones de cadenas. Y falle rápidamente, no lleve el estado roto en todo el sistema. Los rastreos y registros de registro con ID único pueden explicar el flujo / estado de la aplicación.
gavenkoa 18/06/18
23

En esta pregunta más reciente, señalé que las excepciones no deberían contener ningún mensaje. En mi opinión, el hecho de que lo hagan es un gran error. Lo que propongo es que

El "mensaje" de la excepción es el nombre de clase (totalmente calificado) de la excepción.

Una excepción debe contener dentro de sus propias variables miembro tantos detalles como sea posible sobre exactamente lo que sucedió; por ejemplo, un IndexOutOfRangeExceptiondebe contener el valor de índice que se consideró no válido, así como los valores superior e inferior que eran válidos en el momento en que se lanzó la excepción. De esta manera, usando la reflexión, puede tener un mensaje construido automáticamente que se lee así: IndexOutOfRangeException: index = -1; min=0; max=5y esto, junto con el seguimiento de la pila, debe ser toda la información objetiva que necesita para solucionar el problema. Formatearlo en un mensaje bonito como "índice -1 no estaba entre 0 y 5" no agrega ningún valor.

En su ejemplo particular, la NodePropertyNotFoundExceptionclase contendría el nombre de la propiedad que no se encontró y una referencia al nodo que no contenía la propiedad. Esto es importante: debe no contener el nombre del nodo; Debe contener una referencia al nodo real. En su caso particular, esto puede no ser necesario, pero es una cuestión de principios y una forma preferida de pensar: la preocupación principal al construir una excepción es que debe ser utilizable por código que pueda atraparla. La usabilidad por parte de los humanos es una preocupación importante, pero solo secundaria.

Esto se encarga de la situación muy frustrante que pudo haber presenciado en algún momento de su carrera, en la que puede haber detectado una excepción que contiene información vital sobre lo que sucedió dentro del texto del mensaje, pero no dentro de sus variables miembro, por lo que tuvo que hacer un análisis de cadena del texto para averiguar qué sucedió, esperando que el texto del mensaje permanezca igual en futuras versiones de la capa subyacente, y rezando para que el texto del mensaje no esté en algún idioma extranjero cuando su programa esté correr en otros países.

Por supuesto, dado que el nombre de clase de la excepción es el mensaje de la excepción (y las variables miembro de la excepción son los detalles específicos), esto significa que necesita muchas excepciones para transmitir todos los mensajes diferentes, y eso está bien.

Ahora, a veces, mientras escribimos código, nos encontramos con una situación errónea para la que solo queremos codificar rápidamente una throwdeclaración y continuar escribiendo nuestro código en lugar de tener que interrumpir lo que estamos haciendo para crear una nueva clase de excepción para que podamos tíralo ahí mismo. Para estos casos, tengo una GenericExceptionclase que, de hecho, acepta un mensaje de cadena como parámetro de tiempo de construcción, pero el constructor de esta clase de excepción está adornado con un gran gran FIXME XXX TODOcomentario púrpura brillante que indica que cada instancia de esta clase debe ser reemplazado con una instanciación de alguna clase de excepción más especializada antes de que se libere el sistema de software, preferiblemente antes de que se comprometa el código.

Mike Nakis
fuente
8
Si está en un lenguaje que no tiene un GC, como C ++, debe tener mucho cuidado al poner referencias a datos arbitrarios en excepciones que envía a la pila. Lo más probable es que lo que sea que esté haciendo referencia haya sido destruido cuando se detecte la excepción.
Sebastian Redl
44
@SebastianRedl Cierto. Y lo mismo podría aplicarse a C # y Java si el nodeobjeto está protegido por una cláusula use-disposable (en C #) o try-with-resources (en Java): el objeto almacenado con la excepción sería desechado / cerrado, lo que lo hace ilegal acceder a él para obtener información útil en el lugar donde se maneja la excepción. Supongo que en estos casos algún tipo de resumen del objeto debe almacenarse dentro de la excepción, en lugar del objeto en sí. No puedo pensar en una forma infalible de manejar esto genéricamente para todos los casos.
Mike Nakis
13

Como regla general, una excepción debería ayudar a los desarrolladores a identificar la causa al proporcionar información útil (valores esperados, valor real, posibles causas / solución, etc.).

Se deben crear nuevos tipos de excepción cuando ninguno de los tipos integrados tiene sentido . Un tipo específico permite a otros desarrolladores detectar una excepción específica y manejarla. Si el desarrollador supiera cómo manejar su excepción pero el tipo es Exception, no podrá manejarlo correctamente.

mbillard
fuente
+1: los valores esperados frente a los valores reales son extremadamente útiles. En el ejemplo dado en la pregunta, no se debe simplemente decir que un método falló, pero por qué ha fallado (básicamente, el comando exacto que falló, y las circunstancias que causaron la falla.)
Felix Dombek
4

En .NET nunca throw new Exception("...")(como ha mostrado el autor de la pregunta). La excepción es el tipo de excepción raíz y no se supone que se lance directamente. En su lugar, lanza uno de los tipos de excepción .NET derivados o crea tu propia excepción personalizada que se derive de Exception (u otro tipo de excepción).

¿Por qué no lanzar Exception? ¡Porque lanzar Excepción no hace nada para describir su excepción y obliga a su código de llamada a escribir código como el catch(Exception ex) { ... }cual generalmente no es algo bueno! :-).

bytedev
fuente
2

Las cosas que desea buscar para "agregar" a la excepción son aquellos elementos de datos que no son inherentes a la excepción o al seguimiento de la pila. Si esos son parte del "mensaje" o deben adjuntarse cuando se registra es una pregunta interesante.

Como ya ha notado, la excepción probablemente le dice qué, el stacktrace probablemente le dice dónde, pero el "por qué" puede estar más involucrado (debería ser, uno esperaría) que simplemente mirar una o dos líneas y decir " doh! Por supuesto ". Esto es aún más cierto cuando se registran errores en el código de producción: con demasiada frecuencia he sido mordido por datos incorrectos que se han introducido en un sistema en vivo que no existe en nuestros sistemas de prueba. Algo tan simple como saber cuál es la ID del registro en la base de datos que está causando (o contribuyendo) al error puede ahorrar una cantidad significativa de tiempo.

Entonces ... Enumerado o, para .NET, agregado a la recopilación de datos de excepciones registradas (cf @Plip!):

  • Parámetros (esto puede ser un poco interesante: no puede agregar a la recopilación de datos si no se serializa y, a veces, un solo parámetro puede ser sorprendentemente complejo)
  • Los datos adicionales devueltos por ADO.NET o Linq a SQL o similar (¡esto también puede ser un poco interesante!).
  • Cualquier otra cosa podría no ser aparente.

Algunas cosas, por supuesto, no sabrá que necesita hasta que no las tenga en su informe / registro de errores inicial. Algunas cosas no se dan cuenta de que puede obtener hasta que descubra que las necesita.

Murph
fuente
0

¿Cuáles son excepciones para ?

(1) ¿Decirle al usuario que algo salió mal?

Esto debería ser un último recurso, porque su código debe interceder y mostrarles algo "más agradable" que una excepción.

El mensaje de "error" debe indicar clara y sucintamente qué salió mal y qué puede hacer el usuario para recuperarse de la condición de error.

por ejemplo, "No presione este botón nuevamente"

(2) ¿Informarle a un desarrollador cuándo salió mal?

Este es el tipo de cosas que inicia sesión en un archivo para su posterior análisis.
El seguimiento de la pila le dirá al desarrollador dónde se rompió el código; el mensaje debe, nuevamente, indicar qué salió mal.

(3) ¿Decirle a un controlador de excepciones (es decir, código) que algo salió mal?

El Tipo de Excepción decidirá qué controlador de excepciones podrá verlo y las propiedades definidas en el objeto Excepción permitirán que el controlador lo maneje.

El mensaje de la excepción es totalmente irrelevante .

Phill W.
fuente
-3

No cree nuevos tipos si puede evitarlo. Pueden causar confusión adicional, complejidad y generar más código para mantener. Pueden hacer que su código tenga que extenderse. El diseño de una jerarquía de excepción requiere mucho tiempo y pruebas. No es un pensamiento posterior. A menudo es mejor usar la jerarquía de excepciones de lenguaje integrada.

El contenido del mensaje de excepción depende del receptor del mensaje, por lo que debe ponerse en el lugar de esa persona.

Un ingeniero de soporte deberá poder identificar la fuente del error lo más rápido posible. Incluya una cadena descriptiva corta más cualquier dato que pueda ayudar a solucionar el problema. Incluya siempre el seguimiento de la pila, si puede, esta será la única fuente verdadera de información.

La presentación de errores a los usuarios generales de su sistema depende del tipo de error: si el usuario puede solucionar el problema, proporcionando una entrada diferente, por ejemplo, entonces se requiere un mensaje descriptivo conciso. Si el usuario no puede solucionar el problema, lo mejor es indicar que se ha producido un error y registrar / enviar un error al servicio de asistencia (utilizando las pautas anteriores).

Además, no arroje un gran "¡ERROR DE MEDIO!" icono. Es un error, no es el fin del mundo.

En resumen: piense en los actores y use casos para su sistema. Ponte en el lugar de esos usuarios. Se útil Se bueno. Piense en esto por adelantado en el diseño de su sistema. Desde la perspectiva de sus usuarios, estos casos excepcionales y la forma en que el sistema los maneja son tan importantes como los casos normales en su sistema.

Conor
fuente
10
Estoy en desacuerdo. Hay muchas buenas razones para implementar sus propias excepciones cuando el lenguaje API no cubre sus necesidades exactas. Una razón es que en un método en el que varias cosas pueden fallar, puede escribir diferentes cláusulas catch para diferentes tipos de excepciones, donde puede reaccionar ante el problema exacto. Otra es que puede separar varias capas de excepciones que representan diferentes capas de abstracción, donde la capa exacta a la que pertenece una excepción se puede codificar en su tipo. Simplemente usando "Exception" o "IllegalStateException" y una cadena de mensaje no ayuda mucho allí.
Felix Dombek
1
Yo tampoco estoy de acuerdo. Los tipos de excepción deben tener sentido para el código de llamada que lo consumirá. Por ejemplo, si estoy llamando a un marco y esto provoca una FileDoesNotExistException internamente, entonces esto podría no tener ningún sentido para mí como llamador del marco. En cambio, podría ser mejor crear una excepción personalizada y pasar la excepción lanzada como la excepción interna.
bytedev