Mal Manejo de Errores: El desacierto de los 10 Billones de dólares
Si NULL es el desacierto del billón de dólares
¿una mala estrategia de manejo de errores qué sería?
Manejo de Errores
Historias de Terror
¡Los riesgos son serios!
¿Recordamos la historia de Volksvagen?
Lineamientos Generales
Tipos de Errores
- Errores de Funcionalidades
- Errores de Código
No hay un único mecanismo para manejar todos los errores
https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/One_Ring_Blender_Render.png/250px-One_Ring_Blender_Render.png
Errores de Funcionalidad
- ¡El software no debe terminar...
- ...pero tampoco quedar en un estado inestable!
- El usuario debe entender la naturaleza del error
- Los desarrolladores deben entender la naturaleza del error
- Debemos poder separar fácilmente el código donde se emite el error y el código en donde lo manejamos
Errores de Código
- Esta bien que el programa termine:
¡Es fundamental no esconder el error! - ¡La solución real es cambiar el código!
El manejo de errores NO necesita de alto desempeño
¿Son las excepciones un factor a considerar en cuanto a desempeño?
- SÍ: Muchos artículos y videos online
- NO: Libros de Packt, O'Reilly y
Oracle Press sobre desempeño
... pero el Manejo de Errores tampoco debe ser usado como un instrumento para el flujo de ejecución de un programa
Mecanismos de Manejo de Errores
Excepciones
- Unchecked: Errores de Código
- Checked: Errores de Funcionalidad
- Son parte del lenguaje desde la versión 1.0
- Es parte de lo que se enseña como Java "básico"
- Excelente apoyo del compilador
y herramientas de análisis de código - Es fácil manejar muchas excepciones
No gustan las
excepciones checked
porque...
- ...no permiten que los métodos por las que atraviesan sean compuestos
- ...rompen el encapsulamiento: si una excepción que atraviesa métodos, cambia, todos estos métodos deben cambiar
El software es un diseño
de pesos y contrapesos
Las excepciones impiden componer las funciones
- Composición: si existe una función A →B y una función B→C entonces puedo componer fácilmente ambas funciones para tener una función A→C, p.j
f(g(x)) -
Composición con Excepciones: ¿cómo componemos
A →B throws E y una función B→C ? La respuesta es "difícil" porque manejar Errores es difícil
Las excepciones checked rompen el encapsulamiento
- SOLID - Módulos de alto nivel no deben importar nada de módulos de bajo nivel: Excepciones de bajo nivel deben ser relanzadas como excepciones de alto nivel cuando van a viajar de una capa a otra del código
Así no queramos, tenemos que aprender el manejo adecuado de excepciones
Either<L,R>
- Tipo de dato que representa uno de 2 valores posibles
- Un dato puede ser una excepción, usualmente L
- Todo el código intermedio entre dónde se lanza la excepción y dónde se maneja, tiene que ser modificado para utilizar Either, piensa en código con Optional vs sin Optional
- Todas las excepciones deben tratarse como una sola (pattern matching ayuda al manejarlas)
- No hay apoyo del compilador o herramientas estáticas de análisis de código
- No hay librería estándar
- No es parte de la programación "básica"
Try<T>
- Tipo de dato que representa a T o una excepción
- Todo el código intermedio entre dónde se lanza la excepción y dónde se maneja, tiene que ser modificado para utilizar Try, piensa en código con Optional vs sin Optional
- Todas las excepciones deben tratarse como una sola (pattern matching ayuda al manejarlas)
- No hay apoyo del compilador o herramientas estáticas de análisis de código
- No hay librería estándar
- No es parte de la programación "básica"
Las excepciones son la peor manera de manejar errores - salvo todas las demás que han sido intentadas
No hay ningún lenguaje
que tape los errores
del programador
Excepciones
Excepciones
- Cuando una excepción es lanzada...
- ...el ambiente de ejecución, mientras que no encuentre una función que maneje la excepción, desapila el Stack Frame en ejecución
- mientras tanto va creando un Stack Trace
que está disponible programáticamente - si la excepción no es atrapada nunca,
el programa termina y el Stack Trace se
muestra en la salida estándar
Excepciones Checked
- Son parte de la firma de un método,
los errores NO se escoden - Es fácil ver TODAS las excepciones
que un método puede lanzar - El compilador nos recuerda que
deben ser manejadas o propagadas - Las herramientas de análisis de
código las entienden - Perfectas para errores de funcionalidad
Excepciones Unchecked
- No son parte de la firma de un método
dado que NO deben ser atrapadas - Perfectas para manejo de errores de código
- Cuando suceden generan RUIDO lo que
facilita que los programadores las solucionemos
Principio General
La estrategia de manejo de errores NO puede ser un pensamiento apresurado
Principio General
Si se ve raro, seguramente lo estás haciendo mal
try {
// código
try {
// código
} catch() {
// código
}
} catch () {
// código
}
try {
// código
} catch () {
// código
try {
// código
} catch() {
// código
}
}
// código
try {
// código
} catch () {
// código
}
// código
try {
// código
} catch() {
// código
}
// código
Debe ser simple entender TODOS los flujos de ejecución
Exception-Safe Java
Escribe inicialmente el "happy path"
- Garantizas que no se mezcle el manejo
de errores con el flujo normal del programa - Dada la opción de atrapar una excepción
o agregarla a la firma, escoge la última - Luego, por cada excepción que estés lanzando, retrocede borrando la excepción y
según tu estrategia escribe el catch correspondiente
Esta no debe ser
una decisión "obvia"
¿Dónde atrapar?
- Transformar en un mensaje:
Capa de Presentación con tanta información como sea posible - Logearse:
Funcionalidad no crítica, en la capa donde sucede en el lugar con toda la información relevante - Reintentar:
En la capa donde sucede, muy cerca de donde la funcionalidad inicia - Atrapar e "Ignorar":
Si es una excepción "imposible" de suceder, lanza un AssertionError, sino escribe un mensaje explicando - Errores de Código:
No hagas nada en ambientes de desarrollo,
en otros ambientes usa tu FW para logear
sin atraparlos en tu código
public T foo(...) {
try { // nada antes de try
// algoritmo a ejecutar
} catch(...) { // x n
// manejo de errores
} // nada después del último catch
}
¿Cómo atrapar?
public T foo(...) {
try {
doFoo(...);
} catch(...) { // x n
// manejo de errores
}
}
private T doFoo(...) {
// código solo con
// reglas de negocio
}
Aún mejor
Un solo try + foo / doFoo
- Flujo de ejecución es trivial de entender
- Hay una excelente separación entre código
y el código que maneja los errores - El punto inicial del método es cómo puede fallar y cómo vamos manejar los errores, doFoo nos da los detalles
Divide tus métodos para tener control del manejo
de sus errores
Valida SIEMPRE
tus parámetros
- Funciones y especialmente Constructores
- Utiliza excepciones unchecked
- Considera usar una librería
o escribe tus funciones utilitarias
Validación Básica
public Fracción(int numerador, int denominador) {
if (denominador == 0) {
var mensaje = "Denominador no puede ser cero";
throw new IllegalArgumentException(mensaje);
}
}
El problema
T foo(Theaker theaker, Withms withms, Cheedy cheedy) {
if (theaker ...) {
var mensaje = ...;
throw new IllegalArgumentException(mensaje);
}
if (withms ...) {
var mensaje = ...;
throw new IllegalArgumentException(mensaje);
}
if (cheedy ...) {
var mensaje = ...;
throw new IllegalArgumentException(mensaje);
}
// ¡por fin, código!
}
Librerías al rescate
public Fracción(int numerador, int denominador) {
Precondiciones.vericar(numerador != 0, "Numerador no puede ser 0");
// código
}
El problema revisado
T foo(Theaker theaker, Withms withms, Cheedy cheedy) {
Precondiciones.verificar(theaker ..., "");
Precondiciones.verificar(withms ..., "");
Precondiciones.verificar(cheedy ..., "");
// ¡por fin, código!
}
Alternativa
T foo(Theaker theaker, Withms withms, Cheedy cheedy) {
verificarPrecondiciones(theaker, withms, cheedy);
// código
}
private static void verificarPrecondiciones(...) {
Precondiciones.verificar(theaker ..., "");
Precondiciones.verificar(withms ..., "");
Precondiciones.verificar(cheedy ..., "");
}
Métodos aditivos
public static void verificarNoNulo(Object obj) {
if (obj == null) {
throw new IllegalArgumentException("...");
}
}
public <T> static void verificarListaNoVacía(List<T> list) {
verificarNoNulo(list);
if (list.isEmpty()) {
throw new IllegalArgumentException("...");
}
}
public <T> static void listaIniciaConMarcador(List<String> list) {
verificarListaNoVacía(list);
if (!list.get(0).equals(">>>")) {
throw new IllegalArgumentException("...");
}
}
Objects
public class Andinar(Moslaves moslaves, Seledge seledge) {
this.moslaves = Objects.requireNonNull(moslaves, "...");
this.seledge = Objects.requireNonNull(seledge, "...");
}
¡NO atrapes
excepciones unchecked!
- El usuario no las ve si haces
un correcto ciclo de desarrollo - Es muy, muy fácil esconder
el error y olvidarlo - En ambientes que no sean de desarrollo permite que el FW tenga un mecanismo para manejar las excepciones no manejadas. Pero SOLO en el ambiente de Producción y en Stage
NUNCA escribas un método que acepte NULL
- Es muy fácil esconder los errores
- Parámetros opcionales: Escribe métodos sobrecargados que acepten diferentes listas de parámetros
- Obliga a que el código sea específico en qué versión se debe llamar
😞
public static void foo(String s) {
if (s == null) {
s = "{}"; // valor por defecto
}
// más código
}
* * *
var miEse = bar(); // por un error es null!
foo(miEse); // el método no falla
// silenciamos el error!
public static void foo(String s) {
Objects.requiereNonNull(s, "...");
// más código
}
public static void foo() {
return foo("{}");
}
* * *
var miEse = bar(); // por un error es null
foo(miEse); // excepción lanzada
SIEMPRE escribe un mensaje de error
- "Algo salió mal" es un pésimo mensaje
- Describe claramente lo que pasó,
evita mensajes que enumeren diferentes
posibles razones - Incluye el estado actual de los
parámetros involucrados - Valida tus mensajes viendo tu código fallar
Tratas de comprar en una página web y no tienes saldo ¿qué mensaje escribes?
"La transacción por $X no tiene fondos suficientes en la tarjeta ACME terminada en 1234. Mensaje original: ..."
public class Transacción {
private BigDecimal monto;
private Tarjeta tarjeta;
// más atributos
public String toString() {
return "Transacción[monto=" + monto + ",tarjeta=" + tarjeta + "]";
}
}
* * *
var mensaje = "Fondos Insuficientes. Estado de la transacción "
+ transacción + ". Error original: " + e.getMessage();
NUNCA atrapes Exception
- ¿Un mecanismo para todo tipo de errores?
- Atrapa únicamente las excepciones que
esperas que van a suceder - Utiliza multicatch para no repetir código en las claúsulas catch
- Sólo Producción y Stage necesitan una red de seguridad que evite al usuario ver un Stack Trace y debe ser habilitado desde el FW
Silenciar Errores
try {
var sunnota = new Sunnota(o);
var befals = sunnota.getBefals();
var vation = befals.getVation();
var sovely = new Sovely(ot);
return soverly.freages();;
} catch (Exception e) {
// 🤔
// ¿Cuáles excepciones son realmente esperadas?
// ¿Cuáles excepciones nos sorprenderían?
}
Silenciar Errores
try {
var sunnota = new Sunnota(o);
var befals = getBefals(sunnota); // ACMEException
var vation = befals.getVation();
var sovely = new Sovely(ot);
return soverly.freages(); // A113Exception
} catch (AcmeException e) {
// ¡no hay donde esconderse!
} catch (A113Exception e) {
// ¡no hay donde esconderse!
}
En el futuro...
try {
var sunnota = new Sunnota(o);
var befals = getBefals(sunnota); // ACMEException
var vation = befals.getVation(); // THXException
var sovely = new Sovely(ot);
return soverly.freages(); // A113Exception
} catch (AcmeException e) { // NO compila no manejamos THXException
} catch (A113Exception e) {
}
Multicatch
} catch (ACMEException | A113Exception e) {
// no hay repetición de código
}
NUNCA lances Exception
- Si lo lanzas, alguien lo tiene que atrapar
- Esconde los errores que si pueden suceder
private T foo(...) {
var baz = null;
try {
// código
} catch(...) {
// manejo de errores
}
return baz; // 😟
}
Evita un solo return
private T foo(...) {
try {
var baz = ...;
// código
return baz;
} catch(...) {
// manejo de error
return null;
}
}
En un catch retorna
un literal o una constante
public Optional<T> foo(...) {
try {
var baz = ...;
// código
return Optional.of(baz);
} catch(...) {
// manejo de error
return Optional.empty();
}
}
Retorna Optional en métodos públicos
public T foo(...) {
// código
var matchip = ...; // tenemos un Matchip
var declain = ...; // tenemos un Declain
return doFoo(matchip, declain);
}
private T doFoo(Matchip matchip, Declain declain) {
try {
// código que usa
// matchip y declain
} catch (ACMEException e) {
log.error(...); // algo con matchip y declain
}
}
Crea una nueva función
para logear variables
que se crean en el try
Arropa las excepciones cuando pasan de una
capa / módulo a otro
- Protege el encapsulamiento
- Siempre incluye la excepción original
en el constructor de la excepción nueva
public class UsuarioDAO {
// más código
public Usuario porId() throws PersistenceException {
try {
// código
} catch (SQLException e) {
throw new PersistenceException(e);
}
}
}
Logea en un solo lugar
- Tener logs en diferentes lugares dificulta
la lectura de los logs - Crea una excepción para capturar información que solo está disponible a bajo nivel y relanza la excepción
public void foo() throws ACMEException {
try {
// código
} catch (ACMEException e) {
// info solo disponible en este método
log.trace(...);
throw e;
}
}
* * *
try {
// código
foo();
// código
} catch (ACMEException e) {
// todo el resto de la información
log.trace(...);
}
🙅
Lanza una excepción teniendo en cuenta cómo va a ser manejada
- Si una excepción es de bajo nivel,
re-lánzala como una excepción de alto nivel - Si el error no debe viajar, considera un valor de retorno especial
- Si puedes lanzar un subtipo y un supertipo, lanza ambos solo si tienen manejo diferente
Crea una Excepción si necesitas transportar información
- Las Excepciones son POJO
- En vez de crear un mensaje con la información, provee accesores para extraer la información donde se va a crear el mensaje en el idioma deseado
try {
} catch (ACMEException e) {
// toda la información va en el mensaje!
var mensaje = ... + monto + ...
+ tarjeta.últimos4Dígitos() + ... + e.getMessage();
throw new TransactionException(mensaje); // 😮
}
🙅
public class TransacciónExcepción extends Exception {
private BigDecimal monto;
private String últimos4Dígitos;
public TransacciónExcepción(BigDecimal monto, String últimos4Dígitos, ACMEException e) {
super(origin);
this.monto = monto;
this.últimos4Dígitos = últimos4Dígitos;
}
public BigDecimal monto() {
return monto;
}
public String últimos4Dígitos() {
return últimos4Dígitos;
}
}
* * *
try {
// código
} catch (TransacciónException e) {
var monto = e.getMonto();
var últimos4Dígitos = e.getÚltimos4Dígitos();
var mensajeOriginal = e.getCause().getMessage();
// mensaje en cualquier idioma, fácil de cambiar
}
Crea una Excepción si necesitas separar 2 errores que quedarían agrupados
public void foo(...) throws IOException {}
public void bar(...) throws IOException {}
public void baz(...) throws IOException {
foo(); // ¿cómo manejo este error ...
bar(); // ... sin mezclarlo con este?
}
public void foo(...) throws IOException {}
public void bar(...) throws WrappedException {
try {
// código
} catch (IOException e) {
throw new WrappedException(e);
}
}
public void baz(...) throws WrappedException, IOException {
foo(); // catch IOException
bar(); // catch WrappedException
}
Usa Try-with-resources para evitar leaks
Try-with-resources
- Todas las variables creadas en una sentencia
try-with-resource deben implementar Autoclosable - Cuando el bloque del try-with-resources termine,
el método close() de la variable es llamado,
no importa la razón de por qué termino - close() puede ser usado para evitar leaks
try (var fileInpuStream =
new FileInputStream("/path/al/archivo");
// utiliza fileInputStream
}
public static void main(String[] args) {
try (var hal = new Hal9000();
var p2501 = new Project2501(hal)) {
// código que no imprime nada
} // close() se llama en este punto
}
static class Hal9000 implements AutoCloseable {
public void close() throws RuntimeException {
System.out.println("Ciao Hal");
}
}
static class Project2501 implements AutoCloseable {
Project2501(Hal9000 hal) {}
public void close() throws RuntimeException {
System.out.println("Ciao 2501");
throw new RuntimeException("😔");
}
}
Recapitulando
Manejo de Errores
- Antes de codificar define la estrategia
- Primero escribe el happy path
- Por Excepción, en orden inverso de
invocación modifica el código para atrapar
la excepción en el lugar indicado - Escribe un mensaje de error con la descripción
del error, el contexto en el que sucede
y el estado de los objetos relevantes - Crea Excepciones y relanzalas para transportar información hasta el mensaje que escribas
- SIEMPRE valida tus parámetros
- NO atrapes las excepciones unchecked
en ambientes de desarrollo
Q&A
@gaijinco
¡Gracias!
Mal Manejo de Errores: El desacierto de los 10 billones de dólares
By Carlos Obregón
Mal Manejo de Errores: El desacierto de los 10 billones de dólares
- 124