Deberías usar ambos. La cuestión es decidir cuándo usar cada uno .
Hay algunos escenarios en los que las excepciones son la opción obvia :
En algunas situaciones, no puede hacer nada con el código de error , y solo necesita manejarlo en un nivel superior en la pila de llamadas , generalmente solo registra el error, muestra algo al usuario o cierra el programa. En estos casos, los códigos de error requerirían que usted agregue los códigos de error manualmente nivel por nivel, lo que obviamente es mucho más fácil de hacer con las excepciones. El caso es que esto es para situaciones inesperadas e imposibles de manejar .
Sin embargo, en la situación 1 (en la que ocurre algo inesperado e imposible de manejar, simplemente no desea registrarlo), 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 conozco 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 lanzar y no tragar .
La primera regla del manejo de excepciones: no se trague las excepciones . Además, tenga en cuenta que mi captura interna no necesita registrar nada porque la captura externa tendrá todo el seguimiento de la pila y puede registrarlo.
En algunas situaciones, tiene una secuencia de comandos, y si alguno de ellos falla , debe limpiar / eliminar los recursos (*), ya sea que se trate de una situación irrecuperable (que debe lanzarse) o una situación recuperable (en cuyo caso puede manejar localmente o en el código de la persona que llama, pero no necesita excepciones). Obviamente, es mucho más fácil poner todos esos comandos en un solo intento, en lugar de probar los códigos de error después de cada método y limpiarlos / eliminarlos en el bloque final. Tenga en cuenta que si desea que aparezca el error (que es probablemente lo que desea), ni siquiera necesita detectarlo; solo use el finalmente para limpiar / desechar ; solo debe usar catch / retrow si lo desea para agregar información contextual (ver viñeta 2).
Un ejemplo sería una secuencia de sentencias SQL dentro de un bloque de transacciones. Nuevamente, esta también es una situación "inmanejable", incluso si decide detectarla temprano (trátela localmente en lugar de burbujear hasta la parte superior), sigue siendo una situación fatal en la que el mejor resultado es abortar todo o al menos abortar una gran parte del proceso.
(*) Esto es como el on error goto
que usamos en el antiguo Visual Basic
En los constructores solo puedes lanzar excepciones.
Dicho esto, en todas las demás situaciones en las que está devolviendo información sobre la que la persona que llama PUEDE / DEBE tomar alguna acción , usar 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 llama de inmediato, y difícilmente será necesario que los aumente demasiados niveles en la pila.
Por supuesto, siempre es posible tratar los errores esperados como excepciones y detectarlos inmediatamente un nivel por encima, y también es posible abarcar cada línea de código en un intento, detectar y tomar acciones para cada posible error. En mi opinión, este es un mal diseño, no solo porque es mucho más detallado, sino especialmente porque las posibles excepciones que podrían arrojarse no son obvias sin leer el código fuente, y las excepciones podrían arrojarse desde cualquier método profundo, creando gotos invisibles . Rompen la estructura del código creando múltiples puntos de salida invisibles que dificultan la lectura e inspección del código. En otras palabras, nunca debe usar excepciones como control de flujo, porque sería difícil para otros entender y mantener. Puede resultar incluso difícil comprender todos los posibles flujos de código para realizar pruebas.
Nuevamente: para una limpieza / eliminación correcta, puede usar try-finalmente sin atrapar nada .
La crítica más popular sobre los códigos de retorno es que "alguien podría ignorar los códigos de error, pero en el mismo sentido, alguien también puede aceptar excepciones. El manejo de excepciones incorrectas es fácil en ambos métodos. Pero escribir un buen programa basado en códigos de error es mucho más fácil que escribir un programa basado en excepciones . Y si, por cualquier motivo, uno decide ignorar todos los errores (los antiguos on error resume next
), puede hacerlo fácilmente con códigos de retorno y no puede hacerlo sin un montón de repetición de intentos y capturas.
La segunda crítica más popular sobre los códigos de retorno es que "es difícil hacer burbujas", pero eso se debe a que la gente no entiende que las excepciones son para situaciones no recuperables, mientras que los códigos de error no.
Decidir 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 decida incluirlo en una excepción (posiblemente agregando información) y dejar que aparezca. Pero es un error de diseño suponer que TODOS los errores deben presentarse como excepciones.
Para resumirlo:
Me gusta usar excepciones cuando tengo una situación inesperada, en la que no hay mucho que hacer y, por lo general, queremos abortar un gran bloque de código o incluso toda la operación o el programa. Esto es como el antiguo "en error goto".
Me gusta usar códigos de retorno cuando tengo situaciones esperadas en las que el código de la persona que llama puede / debería tomar alguna acción. 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 el "pánico" para situaciones inesperadas fatales, mientras que las situaciones esperadas habituales se devuelven como errores.
Sin embargo, sobre GO, también permite múltiples valores de retorno , que es algo que ayuda mucho al usar códigos de retorno, ya que puede devolver simultáneamente un error y algo más. En C # / Java podemos lograr eso sin parámetros, tuplas o (mi favorito) genéricos, que combinados con enumeraciones pueden proporcionar códigos de error claros para la persona que llama:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.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 un nuevo retorno posible en mi método, incluso puedo verificar a todas las personas que llaman si están cubriendo ese nuevo valor en una declaración de cambio, por ejemplo. Realmente no puedes hacer eso con excepciones. Cuando utilice códigos de retorno, normalmente conocerá de antemano todos los posibles errores y los probará. Con excepciones, por lo general, no sabe 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 en mi opinión, sigue siendo un mal diseño.