Excepciones o códigos de error

Ayer tuve un acalorado debate con un compañero de trabajo sobre cuál sería el método preferido para informar errores. Principalmente, discutíamos el uso de excepciones o códigos de error para informar errores entre capas o módulos de aplicaciones.

¿Qué reglas usas para decidir si lanzas excepciones o devuelves códigos de error para reportar errores?

En cosas de alto nivel, excepciones; en cosas de bajo nivel, códigos de error.

El comportamiento predeterminado de una excepción es desenrollar la stack y detener el progtwig, si estoy escribiendo una secuencia de comandos y busco una clave que no está en el diccionario, probablemente sea un error, y quiero que el progtwig se detenga y me deje saber todo sobre eso.

Sin embargo, si estoy escribiendo un fragmento de código del que debo conocer el comportamiento en todas las situaciones posibles, entonces quiero códigos de error. De lo contrario, tengo que saber cada excepción que puede arrojar cada línea en mi función para saber qué hará (Lea la excepción que puso a tierra una aerolínea para hacerse una idea de lo difícil que es esto). Es tedioso y difícil escribir código que reaccione de forma adecuada a cada situación (incluidas las infelices), pero eso se debe a que escribir código libre de errores es tedioso y difícil, no porque esté pasando códigos de error.

Tanto Raymond Chen como Joel han hecho algunos argumentos eloquents contra el uso de excepciones para todo.

Normalmente prefiero las excepciones, porque tienen más información contextual y pueden transmitir (cuando se usan correctamente) el error al progtwigdor de una manera más clara.

Por otro lado, los códigos de error son más ligeros que las excepciones, pero son más difíciles de mantener. La verificación de errores puede omitirse inadvertidamente. Los códigos de error son más difíciles de mantener porque debe mantener un catálogo con todos los códigos de error y luego encender el resultado para ver qué error se produjo. Los rangos de error pueden ser útiles aquí, porque si lo único que nos interesa es si estamos en presencia de un error o no, es más fácil de verificar (por ejemplo, un código de error HRESULT mayor o igual a 0 es correcto y menos de cero es falla). Inadvertidamente se pueden omitir porque no hay un forzado programático de que el desarrollador verifique los códigos de error. Por otro lado, no puede ignorar las excepciones.

Para resumir, prefiero las excepciones a los códigos de error en casi todas las situaciones.

Prefiero las excepciones porque

  • interrumpen el flujo de la lógica
  • se benefician de la jerarquía de clases que brinda más funciones / funcionalidades
  • cuando se usa correctamente puede representar una amplia gama de errores (por ejemplo, una InvalidMethodCallException también es una LogicException, ya que ambas ocurren cuando hay un error en su código que debería ser detectable antes del tiempo de ejecución), y
  • se pueden usar para mejorar el error (es decir, una definición de clase FileReadException puede contener código para verificar si el archivo existe o está bloqueado, etc.)

Los llamadores de sus funciones pueden ignorar los códigos de error (y a menudo lo son). Las excepciones al menos los obligan a tratar el error de alguna manera. Incluso si su versión de tratar con él es tener un controlador de captura vacía (suspiro).

Excepciones sobre códigos de error, no hay dudas al respecto. Obtiene muchos de los mismos beneficios de las excepciones que con los códigos de error, pero también mucho más, sin las deficiencias de los códigos de error. El único golpe en las excepciones es que es un poco más sobrecarga; pero en este día y edad, esa sobrecarga debería considerarse insignificante para casi todas las aplicaciones.

Aquí hay algunos artículos discutiendo, comparando y contrastando las dos técnicas:

  • Manejo de excepciones orientado a objetos en Perl
  • Excepciones vs. retornos de estado

Hay algunos buenos enlaces en aquellos que pueden darle más lecturas.

Nunca mezclaría los dos modelos … es muy difícil convertir de uno a otro a medida que pasas de una parte de la stack que está usando códigos de error a una pieza más alta que usa excepciones.

Las excepciones son para “cualquier cosa que detenga o impida que el método o la subrutina haga lo que le pediste que haga” … NO devolver mensajes sobre irregularidades o circunstancias inusuales, o el estado del sistema, etc. Usar valores de retorno o ref. (o fuera) parámetros para eso.

Las excepciones permiten que los métodos se escriban (y se utilicen) con semántica que depende de la función del método, es decir, se puede escribir un método que devuelve un objeto Empleado o una Lista de empleados para hacer exactamente eso, y puede utilizarlo llamando.

 Employee EmpOfMonth = GetEmployeeOfTheMonth(); 

Con los códigos de error, todos los métodos devuelven un código de error, por lo que, para aquellos que necesitan devolver algo más para ser utilizado por el código de llamada, debe pasar una variable de referencia para ser poblada con esos datos, y probar el valor de retorno para el código de error, y manejarlo, en cada llamada a función o método.

 Employee EmpOfMonth; if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR) // code to Handle the error here 

Si codifica para que cada método haga una y solo una cosa simple, debe lanzar una excepción siempre que el método no pueda lograr el objective deseado del método. Las excepciones son mucho más ricas y fáciles de usar de esta manera que los códigos de error. Su código es mucho más limpio: el flujo estándar de la ruta del código “normal” puede dedicarse estrictamente al caso en que el método PUEDE lograr lo que usted quería que hiciera … Y luego el código para limpiar o manejar el Las circunstancias “excepcionales” cuando ocurre algo malo que impide que el método se complete con éxito pueden separarse del código normal. Además, si no puede manejar la excepción donde ocurrió, y debe pasarla por la stack a una IU, (o peor, a través del cable desde un componente de nivel medio a una IU), entonces con el modelo de excepción, usted no necesita codificar todos los métodos intermedios en su stack para reconocer y pasar la excepción a la stack … El modelo de excepción lo hace para usted automágicamente … Con códigos de error, esta pieza del rompecabezas puede hacerse onerosa muy rápidamente .

Puede haber algunas situaciones donde usar las excepciones de una manera limpia, clara y correcta es engorroso, pero la gran mayoría de las excepciones de tiempo son la elección obvia. El mayor beneficio que el manejo de excepciones tiene sobre los códigos de error es que cambia el flujo de ejecución, lo cual es importante por dos razones.

Cuando ocurre una excepción, la aplicación ya no sigue su ruta de ejecución ‘normal’. La primera razón por la que esto es tan importante es que, a menos que el autor del código salga de su camino para ser malo, el progtwig se detendrá y no continuará haciendo cosas impredecibles. Si no se verifica un código de error y no se toman las medidas adecuadas en respuesta a un código de error incorrecto, el progtwig seguirá haciendo lo que está haciendo y quién sabe cuál será el resultado de esa acción. Hay muchas situaciones en las que hacer que el progtwig haga “lo que sea” podría terminar siendo muy caro. Considere un progtwig que recupera información de rendimiento para varios instrumentos financieros que vende una empresa y entrega esa información a corredores / mayoristas. Si algo sale mal y el progtwig continúa, podría enviar datos de rendimiento erróneos a los corredores y mayoristas. No conozco a nadie más, pero no quiero ser el único sentado en una oficina de VP explicando por qué mi código hizo que la compañía obtuviera multas regulatorias de 7 cifras. En general, es preferible enviar un mensaje de error a los clientes que entregar datos incorrectos que podrían parecer ‘reales’, y la última situación es mucho más fácil de encontrar con un enfoque mucho menos agresivo, como los códigos de error.

La segunda razón por la que me gustan las excepciones y su ruptura de la ejecución normal es que hace mucho, mucho más fácil mantener la lógica de ‘cosas normales están sucediendo’ separada de la ‘lógica de algo salió mal’. Para mí, esto:

 try { // Normal things are happening logic catch (// A problem) { // Something went wrong logic } 

… es preferible a esto:

 // Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic } // Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic } // Some normal stuff logic if (errorCode means error) { // Some stuff went wrong logic } 

Hay otras pequeñas cosas sobre excepciones que también son agradables. Tener un montón de lógica condicional para realizar un seguimiento de si alguno de los métodos invocados en una función tenía un código de error devuelto, y devolver ese código de error más arriba es una gran cantidad de placa de caldera. De hecho, es una gran cantidad de placa de la caldera que puede salir mal. Tengo mucha más fe en el sistema de excepción de la mayoría de los lenguajes que en un nido de ratas de declaraciones de if-else-if-else que escribió Fred ‘fuera de la universidad’, y tengo muchas cosas mejores que hacer con mi tiempo que el código revisando dicho nido de rata.

En el pasado me uní al campo de código de error (hice demasiada progtwigción en C). Pero ahora he visto la luz.

Sí, las excepciones son un poco una carga para el sistema. Pero simplifican el código, reduciendo el número de errores (y WTF).

Entonces usa la excepción pero úsalas sabiamente. Y ellos serán tus amigos.

Como nota al margen. Aprendí a documentar qué excepción se puede arrojar por qué método. Desafortunadamente esto no es requerido por la mayoría de los idiomas. Pero aumenta la posibilidad de manejar las excepciones correctas en el nivel correcto.

Puedo estar sentado en la valla aquí, pero …

  1. Depende del idioma
  2. Cualquiera que sea el modelo que elija, sea consecuente sobre cómo lo usa.

En Python, el uso de excepciones es una práctica estándar, y estoy feliz de definir mis propias excepciones. En C no tienes excepciones en absoluto.

En C ++ (al menos en el lenguaje STL), las excepciones generalmente se producen solo por errores verdaderamente excepcionales (prácticamente nunca los veo). No veo ninguna razón para hacer algo diferente en mi propio código. Sí, es fácil ignorar los valores de retorno, pero C ++ tampoco te obliga a detectar excepciones. Creo que solo tienes que acostumbrarte a hacerlo.

La base de código en la que trabajo es principalmente C ++ y usamos códigos de error en casi todos lados, pero hay un módulo que genera excepciones para cualquier error, incluidos los menos excepcionales, y todo el código que usa ese módulo es bastante horrible. Pero eso podría deberse a que hemos mezclado excepciones y códigos de error. El código que utiliza consistentemente códigos de error es mucho más fácil de usar. Si nuestro código usara consistentemente excepciones, tal vez no sería tan malo. Mezclar los dos no parece funcionar tan bien.

Como trabajo con C ++ y tengo RAII para que sean seguros de usar, utilizo excepciones casi exclusivamente. Extrae el manejo de errores del flujo normal del progtwig y hace que el bash sea más claro.

Sin embargo, dejo excepciones para circunstancias excepcionales. Si estoy esperando que ocurra un cierto error, TryParse() que la operación tendrá éxito antes de realizarla, o llamo a una versión de la función que usa códigos de error (Like TryParse() )

Deberías usar ambos. El asunto es decidir cuándo usar cada uno .

Hay algunos escenarios en los que las excepciones son la elección obvia :

  1. En algunas situaciones, no puede hacer nada con el código de error , y solo necesita manejarlo en un nivel superior en la stack de llamadas , generalmente solo registra el error, muestra algo al usuario o cierra el progtwig. En estos casos, los códigos de error requerirían que los códigos de error se suban de nivel manualmente por nivel, lo que obviamente es mucho más fácil de hacer con las excepciones. El punto es que esto es para situaciones inesperadas e imposibles de manejar .

  2. Sin embargo, sobre la situación 1 (en la que algo inesperado e inmanejable sucede, simplemente desea registrarla), las excepciones pueden ser útiles porque puede agregar información contextual . Por ejemplo, si obtengo una SqlException en mis ayudantes de datos de nivel inferior, querré detectar ese error en el nivel bajo (donde sé el comando SQL que causó el error) para poder capturar esa información y volver a lanzar con información adicional . Tenga en cuenta la palabra mágica aquí: volver a tirar, y no tragar . La primera regla de manejo de excepciones: no tragues excepciones . Además, tenga en cuenta que mi captura interna no necesita registrar nada porque la captura externa tendrá toda la ruta de la stack y puede registrarla.

  3. En algunas situaciones, usted tiene una secuencia de comandos, y si alguno de ellos falla , debe limpiar / eliminar los recursos (*), independientemente de si esta es una situación irrecuperable (que debe arrojarse) o una situación recuperable (en cuyo caso puede manejar localmente o en el código de llamada, pero no necesita excepciones). Obviamente, es mucho más fácil poner todos esos comandos en una sola prueba, en lugar de probar los códigos de error después de cada método, y limpiar / desechar en el bloque finally. Tenga en cuenta que si desea que aparezca el error (que es probablemente lo que desea), ni siquiera tiene que atraparlo; solo tiene que utilizar finalmente para limpiar / desechar , solo debe usar catch / retrow si desea para agregar información contextual (ver viñeta 2).

    Un ejemplo sería una secuencia de sentencias de SQL dentro de un bloque de transacción. De nuevo, esto también es una situación “no manejable”, incluso si decides atraparlo temprano (tratarlo localmente en lugar de burbujear hasta la cima) sigue siendo una situación fatal de donde el mejor resultado es abortar todo o al menos abortar una gran parte del proceso.
    (*) Esto es como el on error goto que utilizamos en Visual Basic antiguo

  4. En constructores solo puedes lanzar excepciones.

Una vez dicho esto, en todas las demás situaciones en las que se devuelve información sobre la que la persona que llama PUEDE / DEBE tomar alguna medida , el uso de códigos de retorno es probablemente una mejor alternativa. Esto incluye todos los “errores” esperados , porque probablemente deberían ser manejados por la persona que llamó de inmediato, y no será necesario subir demasiados niveles en la stack.

Por supuesto, siempre es posible tratar los errores esperados como excepciones, y capturar inmediatamente un nivel arriba, y también es posible abarcar cada línea de código en una captura de prueba y tomar medidas para cada posible error. OMI, esto es un mal diseño, no solo porque es mucho más detallado, sino especialmente porque las posibles excepciones que se pueden lanzar no son obvias sin leer el código fuente, y se pueden generar excepciones desde cualquier método profundo, creando gotos invisibles . Rompen la estructura del código al crear múltiples puntos de salida invisibles que hacen que el código sea difícil de leer e inspeccionar. En otras palabras, nunca debe usar excepciones como control de flujo , porque eso sería difícil de entender y mantener. Incluso puede ser difícil comprender todos los flujos de código posibles para las pruebas.
De nuevo: para una correcta limpieza / eliminación, puede usar try-finally sin capturar nada .

La crítica más popular acerca de los códigos de retorno es que “alguien puede ignorar los códigos de error, pero en el mismo sentido, alguien también puede tragar excepciones. El manejo de excepciones es fácil en ambos métodos. Pero escribir un buen progtwig basado en códigos de error es mucho más fácil. que escribir un progtwig basado en excepciones . Y si, por algún motivo, decide ignorar todos los errores (lo anterior on error resume next ), puede hacerlo fácilmente con los códigos de retorno y no puede hacer eso sin muchas trampas repetitivo.

La segunda crítica más popular acerca de los códigos de retorno es que “es difícil burbujear”, pero eso se debe a que las personas no entienden que las excepciones son para situaciones no recuperables, mientras que los códigos de error no lo son.

La decisión entre excepciones y códigos de error es un área gris. Incluso es posible que necesite obtener un código de error de algún método comercial reutilizable, y luego decide envolverlo en una excepción (posiblemente agregando información) y dejar que suba. Pero es un error de diseño suponer que TODOS los errores deben arrojarse como excepciones.

En resumen:

  • Me gusta usar excepciones cuando tengo una situación inesperada, en la que no hay mucho que hacer, y generalmente queremos abortar un gran bloque de código o incluso toda la operación o el progtwig. Esto es como el viejo “en error goto”.

  • Me gusta usar códigos de retorno cuando esperaba situaciones en las que el código de la persona que llama puede / debe tomar alguna medida. Esto incluye la mayoría de los métodos comerciales, API, validaciones, etc.

Esta diferencia entre excepciones y códigos de error es uno de los principios de diseño del lenguaje GO, que utiliza “pánico” para situaciones inesperadas fatales, mientras que las situaciones regulares esperadas se devuelven como errores.

Sin embargo, sobre GO, también permite múltiples valores de retorno , lo cual es una gran ayuda en el uso de códigos de retorno, ya que simultáneamente puede devolver un error y otra cosa. En C # / Java podemos lograrlo sin parámetros, Tuples o (mi favorito) Generics, que combinados con enumeraciones pueden proporcionar códigos claros de error a la persona que llama:

 public MethodResult CreateOrder(CreateOrderOptions options) { .... return MethodResult.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area"); ... return MethodResult.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order); } var result = CreateOrder(options); if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK) // do something else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS) order = result.Entity; // etc... 

Si agrego una nueva devolución posible en mi método, incluso puedo verificar todas las llamadas si están cubriendo ese nuevo valor en una statement de cambio, por ejemplo. Realmente no puedes hacer eso con excepciones. Cuando utiliza códigos de retorno, generalmente sabrá por adelantado todos los errores posibles y los probará. Con excepciones, generalmente no sabes lo que podría pasar. Envolver enumeraciones dentro de excepciones (en lugar de Genéricos) es una alternativa (siempre que esté claro el tipo de excepciones que arrojará cada método), pero IMO sigue siendo un mal diseño.

Las firmas de métodos deben comunicarle lo que hace el método. Algo así como long errorCode = getErrorCode (); podría estar bien, pero long errorCode = fetchRecord (); es confuso.

Mi razonamiento sería si está escribiendo un controlador de bajo nivel que realmente necesita rendimiento, luego use códigos de error. Pero si usa ese código en una aplicación de nivel superior y puede manejar un poco de sobrecarga, luego ajuste ese código con una interfaz que verifique esos códigos de error y genere excepciones.

En todos los demás casos, las excepciones son probablemente el camino a seguir.

Mi enfoque es que podemos usar ambos, es decir, códigos de Excepciones y Errores al mismo tiempo.

Estoy acostumbrado a definir varios tipos de excepciones (por ejemplo, DataValidationException o ProcessInterruptExcepion) y dentro de cada excepción, definir una descripción más detallada de cada problema.

Un ejemplo simple en Java:

 public class DataValidationException extends Exception { private DataValidation error; /** * */ DataValidationException(DataValidation dataValidation) { super(); this.error = dataValidation; } } enum DataValidation{ TOO_SMALL(1,"The input is too small"), TOO_LARGE(2,"The input is too large"); private DataValidation(int code, String input) { this.input = input; this.code = code; } private String input; private int code; } 

De esta manera, uso Excepciones para definir errores de categoría y códigos de error para definir información más detallada sobre el problema.

Las excepciones son por circunstancias excepcionales , es decir, cuando no forman parte del flujo normal del código.

Es bastante legítimo mezclar Excepciones y códigos de error, donde los códigos de error representan el estado de algo, en lugar de un error en la ejecución del código per se (por ejemplo, verificar el código de retorno de un proceso secundario).

Pero cuando ocurre una circunstancia excepcional, creo que las Excepciones son el modelo más expresivo.

Hay casos en los que puede preferir o tener que usar códigos de error en lugar de Excepciones, y estos ya se han cubierto adecuadamente (a parte de otras limitaciones obvias, como la compatibilidad con el comstackdor).

Pero yendo en la otra dirección, el uso de Excepciones le permite construir abstracciones de nivel aún mayor en el manejo de errores, que pueden hacer que su código sea aún más expresivo y natural. Recomiendo leer este artículo excelente, pero subestimado, del experto en C ++ Andrei Alexandrescu sobre el tema de lo que él llama, “Enforcements”: http://www.ddj.com/cpp/184403864 . Aunque es un artículo de C ++, los principios son generalmente aplicables, y he traducido el concepto de ejecuciones a C # con bastante éxito.

En primer lugar, estoy de acuerdo con la respuesta de Tom de que para cosas de alto nivel use excepciones y para códigos de error de uso de cosas de bajo nivel, siempre que no sea Arquitectura Orientada a Servicios (SOA).

En SOA, donde se pueden llamar los métodos a través de diferentes máquinas, las excepciones no se pueden pasar por el cable, en su lugar, utilizamos las respuestas de éxito / fracaso con una estructura como la siguiente (C #):

 public class ServiceResponse { public bool IsSuccess => string.IsNullOrEmpty(this.ErrorMessage); public string ErrorMessage { get; set; } } public class ServiceResponse : ServiceResponse { public TResult Result { get; set; } } 

Y use esto:

 public async Task> GetUserName(Guid userId) { var response = await this.GetUser(userId); if (!response.IsSuccess) return new ServiceResponse { ErrorMessage = $"Failed to get user." }; return new ServiceResponse { Result = user.Name }; } 

Cuando se usan de manera consistente en las respuestas de su servicio, crea un patrón muy agradable de manejo de éxitos / fracasos en la aplicación. Esto permite un manejo de errores más fácil en llamadas asincrónicas dentro de servicios así como a través de servicios.

Preferiría excepciones para todos los casos de error, excepto cuando una falla es un resultado libre de errores de una función que devuelve un tipo de datos primitivo. Por ejemplo, encontrar el índice de una subcadena dentro de una cadena más grande generalmente devolvería -1 si no se encuentra, en lugar de generar una NotFoundException.

No es aceptable devolver punteros no válidos que puedan desreferenciarse (por ejemplo, causar NullPointerException en Java).

Usar múltiples códigos de error numéricos diferentes (-1, -2) como valores de retorno para la misma función suele ser un estilo incorrecto, ya que los clientes pueden hacer una comprobación “== -1” en lugar de “<0".

Una cosa a tener en cuenta aquí es la evolución de las API a lo largo del tiempo. Una buena API permite cambiar y extender el comportamiento de falla de varias maneras sin romper clientes. Por ejemplo, si un identificador de error de cliente verificó 4 casos de error y agrega un quinto valor de error a su función, es posible que el controlador del cliente no lo pruebe y se rompa. Si genera excepciones, esto generalmente hará que sea más fácil para los clientes migrar a una versión más nueva de una biblioteca.

Otra cosa a considerar es cuando se trabaja en equipo, donde se traza una línea clara para que todos los desarrolladores tomen tal decisión. Por ejemplo, “Excepciones para cosas de alto nivel, códigos de error para cosas de bajo nivel” es muy subjetivo.

En cualquier caso, cuando es posible más de un tipo de error trivial, el código fuente nunca debe usar el literal numérico para devolver un código de error o manejarlo (return -7, si x == -7 …), pero siempre una constante con nombre (return NO_SUCH_FOO, si x == NO_SUCH_FOO).

Si trabajas en un proyecto grande, no puedes usar solo excepciones o solo códigos de error. En diferentes casos, debe usar diferentes enfoques.

Por ejemplo, decide usar solo excepciones. Pero una vez que decide utilizar el procesamiento de eventos asíncrono. Es una mala idea usar excepciones para el manejo de errores en estas situaciones. Pero usar códigos de error en todas partes en la aplicación es tedioso.

Así que mi opinión es que es normal usar excepciones y códigos de error simultáneamente.

Para la mayoría de las aplicaciones, las excepciones son mejores. La excepción es cuando el software tiene que comunicarse con otros dispositivos. El dominio en el que trabajo es el control industrial. Aquí se prefieren y esperan códigos de error. Entonces mi respuesta es que eso depende de la situación.

Creo que también depende de si realmente necesitas información como el seguimiento de la stack del resultado. En caso afirmativo, definitivamente va a Exception, que proporciona un objeto lleno de mucha información sobre el problema. Sin embargo, si solo está interesado en el resultado y no le importa por qué ese resultado, entonces busque el código de error.

Por ejemplo, cuando está procesando un archivo y se enfrenta a IOException, el cliente puede interesarse en saber desde dónde se desencadenó esto, al abrir el archivo o analizar el archivo, etc. Así que es mejor que devuelva IOException o su subclase específica. Sin embargo, en el caso de que tenga un método de inicio de sesión y desea saber si fue exitoso o no, puede devolver booleano o mostrar el mensaje correcto, devolver el código de error. Aquí el Cliente no está interesado en saber qué parte de la lógica causó ese código de error. Solo sabe si su Credencial no es válida o locking de cuenta, etc.

Otro caso de uso en el que puedo pensar es cuando los datos viajan en la red. Su método remoto puede devolver solo el código de error en lugar de Exception para minimizar la transferencia de datos.

Mi regla general es:

  • Solo un error podría aparecer en una función: use un código de error (como parámetro de la función)
  • Más de un error específico podría aparecer: lanzar excepción

Los códigos de error tampoco funcionan cuando su método devuelve algo que no sea un valor numérico …