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

  1. Son parte de la firma de un método,
    los errores NO se escoden
  2. Es fácil ver TODAS las excepciones
    que un método puede lanzar
  3. El compilador nos recuerda que
    deben ser manejadas o propagadas
  4. Las herramientas de análisis de
    código las entienden
  5. Perfectas para errores de funcionalidad

Excepciones Unchecked

  1. No son parte de la firma de un método
    dado que NO deben ser atrapadas
  2. Perfectas para manejo de errores de código
  3. 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