Blog

Excepciones en Java: comprobadas, no comprobadas y try-with-resources

Java divide las excepciones en comprobadas, que el compilador te obliga a manejar, y no comprobadas, que no. Aprende try, catch y finally, cómo lanzar y envolver bien las excepciones, y cómo try-with-resources cierra todo en orden inverso.

Una excepción es la forma en que un método de Java dice “no puedo terminar esto”. Con algunas excepciones el compilador te obliga a hacer algo y con otras no, y esa división define cómo el código Java maneja los fallos. La otra mitad de la historia es la limpieza: cerrar lo que abriste, incluso cuando algo sale mal a mitad de camino.

Este post cubre la jerarquía de excepciones, las excepciones comprobadas y no comprobadas, try, catch y finally, cómo lanzar y envolver excepciones, las excepciones propias y try-with-resources. Termina con tres hábitos que conviene evitar. Cada programa de abajo se ejecutó en Java 25, y su salida está copiada de esa ejecución. Para ejecutar uno tú mismo, guárdalo como Main.java y ejecuta java Main.java.

La jerarquía de excepciones

Toda excepción en Java es un objeto cuya clase extiende Throwable. Si subimos por las clases padre de tres excepciones comunes, vemos el árbol familiar completo:

void main() {
    var types = List.of(
            IOException.class,
            IllegalArgumentException.class,
            StackOverflowError.class);
    for (var type : types) {
        var chain = new ArrayList<String>();
        for (Class<?> c = type; c != null; c = c.getSuperclass()) {
            chain.add(c.getSimpleName());
        }
        IO.println(String.join(" -> ", chain));
    }
}

Imprime:

IOException -> Exception -> Throwable -> Object
IllegalArgumentException -> RuntimeException -> Exception -> Throwable -> Object
StackOverflowError -> VirtualMachineError -> Error -> Throwable -> Object

Debajo de Throwable hay dos ramas:

  • Error significa que la propia JVM tiene problemas: se quedó sin memoria, hubo un desbordamiento de pila o un archivo de clase está dañado. Normalmente tu código no puede arreglar eso, así que no los capturas.
  • Exception significa que algo salió mal y que un programa quizá pueda manejarlo.

Exception se divide una vez más. RuntimeException y sus subclases son no comprobadas (unchecked). Todas las demás Exception son comprobadas (checked). Error también es no comprobada. La diferencia está por completo en lo que el compilador te obliga a hacer.

Excepciones comprobadas: captúrala o declárala

Una excepción comprobada se tiene que capturar o declarar en la cláusula throws del método, y el compilador rechaza el código que no hace ninguna de las dos cosas. new URI(text) puede lanzar la excepción comprobada URISyntaxException, así que esto no compila:

URI parse(String text) {
    return new URI(text);
}

void main() {
    IO.println(parse("https://example.com/docs"));
}

La compilación falla con:

Main.java:2: error: unreported exception URISyntaxException; must be caught or declared to be thrown
    return new URI(text);
           ^

El mensaje nombra las dos salidas. Puedes capturar la excepción y ocuparte de ella donde ocurre, o puedes agregar throws URISyntaxException y convertirla en un problema de quien te llama:

String hostOf(String text) {
    try {
        return new URI(text).getHost();
    } catch (URISyntaxException e) {
        IO.println("bad address: " + e.getMessage());
        return "unknown";
    }
}

URI parse(String text) throws URISyntaxException {
    return new URI(text);
}

void main() throws URISyntaxException {
    IO.println(hostOf("https://example.com/docs"));
    IO.println(hostOf("https://example.com/my docs"));
    IO.println(parse("https://example.org/api").getPath());
}

Imprime:

example.com
bad address: Illegal character in path at index 22: https://example.com/my docs
unknown
/api

hostOf captura la excepción y devuelve un valor de respaldo, así que quienes lo llaman nunca la ven. parse la declara, así que cada llamador tiene que tomar la misma decisión otra vez. main también la declara. Si una excepción saliera de main, el programa se detendría con un stack trace, como mostró la primera parte de esta serie.

Una excepción no comprobada no necesita ninguna de las dos cosas. Integer.parseInt lanza NumberFormatException, que es una RuntimeException, y puedes llamarlo sin try y sin throws.

Explicado como si tuvieras diez años

Una excepción comprobada es una etiqueta de “frágil” en un paquete. Todos los que tocan el paquete tienen que fijarse en la etiqueta. O abres el paquete con cuidado tú mismo, o se lo pasas a otro con la etiqueta todavía a la vista. No puedes despegar la etiqueta y hacer como si no existiera.

Una excepción no comprobada es un paquete sin etiqueta. Igual se puede romper, pero nadie está obligado a pensar en eso por el camino.

La versión precisa

El compilador lleva la cuenta de qué excepciones comprobadas puede lanzar cada sentencia. Lo saca de las cláusulas throws de los métodos y constructores que llama, y de tus propias sentencias throw. Cada una tiene que manejarse con un catch que la rodee, para ese tipo o un tipo padre, o aparecer en la cláusula throws del método que estás escribiendo. La revisión ocurre en tiempo de compilación, y nada de eso existe en el programa en ejecución: la JVM trata igual las excepciones comprobadas y las no comprobadas.

Dónde falla la analogía: una etiqueta real viaja con el paquete, y cualquiera la puede ver. La revisión de Java ocurre solo cuando javac compila código fuente Java. En tiempo de ejecución nadie busca la etiqueta, así que el código compilado desde otro lenguaje de la JVM, o un truco que oculta el tipo, puede lanzar una excepción comprobada a través de un método que nunca la declaró.

Qué tipo usar

La regla habitual es: no comprobadas para errores de programación, comprobadas para situaciones de las que quien llama se puede recuperar razonablemente. Un null donde no se permite, un índice más allá del final o una cantidad negativa son un bug en el código que llama. Obligar a cada llamador a capturarlo solo agregaría ruido, así que esas son RuntimeException. Un archivo que no está, una red que se cae o un texto que un usuario escribió y que no es una dirección válida son cosas que quien llama puede manejar, pidiendo el dato otra vez o usando un valor por defecto. Java hizo que esas fueran comprobadas.

Esa regla todavía se discute. Las excepciones comprobadas hacen visible el fallo en la firma de un método, y el compilador no te deja olvidar ninguna. Pero se propagan por todas las capas de un programa, invitan a escribir bloques catch vacíos solo para que el error desaparezca, y no encajan con las lambdas. forEach(s -> IO.println(new URI(s))) falla con el mismo error unreported exception, porque forEach recibe una interfaz cuyo método no declara excepciones comprobadas. Kotlin, Scala y C# decidieron no tener excepciones comprobadas, y muchas bibliotecas modernas de Java lanzan solo no comprobadas. El propio JDK sigue usando las dos, así que necesitas sentirte cómodo con ambas.

try, catch y finally

Un bloque try ejecuta código que puede lanzar una excepción, y cada catch nombra un tipo que maneja. Un catch puede listar varios tipos separados por |, y eso se llama multi-catch:

int port(String text) {
    try {
        int n = Integer.parseInt(text);
        return List.of(80, 443, 8080).get(n);
    } catch (NumberFormatException | IndexOutOfBoundsException e) {
        IO.println("fallback, because " + e.getClass().getSimpleName());
        return 80;
    }
}

void main() {
    IO.println(port("1"));
    IO.println(port("one"));
    IO.println(port("7"));
}

Imprime:

443
fallback, because NumberFormatException
80
fallback, because ArrayIndexOutOfBoundsException
80

Mira las últimas líneas. Pedimos el elemento 7 de un List.of de tres elementos y esperábamos IndexOutOfBoundsException. En realidad la lista lanzó ArrayIndexOutOfBoundsException, una subclase, porque por dentro usa un arreglo. El catch funcionó igual, porque un catch maneja el tipo nombrado y todas sus subclases. Es una buena razón para capturar el tipo documentado en vez de adivinar la clase exacta.

Los tipos de un multi-catch no pueden estar relacionados. NumberFormatException | IllegalArgumentException falla con Alternatives in a multi-catch statement cannot be related by subclassing, porque el padre ya cubre al hijo.

Un catch amplio antes de uno específico no compila

Los bloques catch se prueban de arriba hacia abajo, y gana el primero cuyo tipo encaja. Así que un tipo padre puesto antes de su propia subclase deja inalcanzable el catch de abajo:

void main() {
    try {
        IO.println(Integer.parseInt("forty"));
    } catch (RuntimeException e) {
        IO.println("something went wrong");
    } catch (NumberFormatException e) {
        IO.println("not a number");
    }
}

La compilación falla con:

Main.java:6: error: exception NumberFormatException has already been caught
    } catch (NumberFormatException e) {
      ^

Pon primero el tipo específico y después el amplio. El compilador también revisa la otra dirección: capturar una excepción comprobada que el cuerpo del try no puede lanzar, como IOException alrededor de un simple IO.println, falla con exception IOException is never thrown in body of corresponding try statement.

finally siempre se ejecuta

Un bloque finally se ejecuta cuando el try termina, ya sea que terminó normalmente, lanzó una excepción o hizo return:

String check(String text) {
    try {
        IO.println("try: parsing " + text);
        Integer.parseInt(text);
        return "number";
    } catch (NumberFormatException e) {
        IO.println("catch: " + e.getMessage());
        return "not a number";
    } finally {
        IO.println("finally: done with " + text);
    }
}

void main() {
    IO.println("result: " + check("42"));
    IO.println("result: " + check("forty"));
}

Imprime:

try: parsing 42
finally: done with 42
result: number
try: parsing forty
catch: For input string: "forty"
finally: done with forty
result: not a number

El return "number" se ejecutó primero y eligió el valor, pero el método no salió hasta que finally imprimió su línea. Solo entonces main recibió el resultado. Lo mismo pasó en el camino que va por catch.

Un return dentro de finally se traga la excepción

Un bloque finally que hace return reemplaza lo que el try estuviera haciendo, y eso incluye una excepción que iba de salida. javac avisa, pero solo si pides advertencias con -Xlint. Esta serie compila cada ejemplo con -Xlint:all -Werror, que convierte la advertencia en una compilación fallida:

int risky() {
    try {
        throw new IllegalStateException("the order was lost");
    } finally {
        return -1;
    }
}

void main() {
    IO.println("risky() returned " + risky());
    IO.println("no exception reached main");
}

La compilación falla con:

Main.java:6: warning: [finally] finally clause cannot complete normally
error: warnings found and -Werror specified

Con un simple javac Main.java, o con java Main.java, no hay ninguna advertencia, y el programa se ejecuta. Para mostrar lo que hace, esta versión silencia la advertencia con @SuppressWarnings("finally"):

@SuppressWarnings("finally")
int risky() {
    try {
        throw new IllegalStateException("the order was lost");
    } finally {
        return -1;
    }
}

void main() {
    IO.println("risky() returned " + risky());
    IO.println("no exception reached main");
}

Imprime:

risky() returned -1
no exception reached main

La IllegalStateException desapareció. Ni stack trace ni mensaje, y quien llamó recibió -1 como si todo estuviera bien. Usa finally solo para limpiar, y nunca hagas return, break ni throw desde ahí.

Lanzar excepciones

Una sentencia throw recibe cualquier objeto Throwable, y lo más útil que puedes poner en él es un mensaje que le diga al lector qué estaba mal. Para argumentos incorrectos, el JDK te da dos herramientas: IllegalArgumentException, y Objects.requireNonNull para null:

record Transfer(String from, String to, long cents) {
    Transfer {
        Objects.requireNonNull(from, "from account is required");
        Objects.requireNonNull(to, "to account is required");
        if (cents <= 0) {
            throw new IllegalArgumentException(
                    "cents must be positive, got " + cents + " for " + from + " -> " + to);
        }
    }
}

void main() {
    IO.println(new Transfer("ana", "ben", 500));
    try {
        new Transfer("ana", null, 500);
    } catch (NullPointerException e) {
        IO.println(e.getMessage());
    }
    new Transfer("ana", "ben", -500);
}

Imprime y se detiene:

Transfer[from=ana, to=ben, cents=500]
to account is required
Exception in thread "main" java.lang.IllegalArgumentException: cents must be positive, got -500 for ana -> ben

requireNonNull devuelve su argumento cuando no es null, y lanza NullPointerException con tu mensaje cuando sí lo es. Sin mensaje, getMessage() devuelve null, y eso no le sirve a nadie.

El mensaje de IllegalArgumentException dice cuál es la regla, qué valor la rompió y a qué transferencia pertenecía. Compáralo con un simple "invalid". Cuando esta línea aparece en un log, la primera versión te dice dónde buscar. La parte sobre records explica los constructores compactos como este.

Envolver una excepción conserva su causa

Cuando capturas una excepción de bajo nivel y lanzas otra con más sentido, pasa la original como causa. Toda excepción estándar tiene un constructor que recibe (String message, Throwable cause):

int readPort(String text) {
    try {
        return Integer.parseInt(text);
    } catch (NumberFormatException e) {
        throw new IllegalStateException("config: port must be a number", e);
    }
}

int readPortLogged(String text) {
    try {
        return readPort(text);
    } catch (IllegalStateException e) {
        IO.println("log:       " + e.getMessage());
        throw e;
    }
}

void main() {
    try {
        readPortLogged("eighty");
    } catch (IllegalStateException e) {
        IO.println("error:     " + e.getMessage());
        IO.println("caused by: " + e.getCause());
    }
}

Imprime:

log:       config: port must be a number
error:     config: port must be a number
caused by: java.lang.NumberFormatException: For input string: "eighty"

Aquí pasan dos cosas. readPort envuelve: la nueva excepción explica el problema en términos de configuración, y getCause() todavía guarda la NumberFormatException original. readPortLogged relanza: registra el error y luego lanza el mismo objeto con throw e;, así que quien llama ve exactamente lo que lanzó readPort.

Si nadie captura una excepción envuelta, el stack trace imprime las dos. Después de los frames de la IllegalStateException, aparece una línea que empieza con Caused by: java.lang.NumberFormatException: For input string: "eighty", seguida de los frames propios de esa excepción y una línea como ... 1 more para los frames que las dos comparten. Lee un stack trace largo desde el último Caused by: hacia arriba. El de más abajo suele ser donde empezó el problema.

Quita la e del constructor y toda esa segunda mitad desaparece. Perder la causa es una de las formas más comunes de alargar una sesión de depuración.

Excepciones propias

Una excepción propia es una clase que extiende Exception, si la quieres comprobada, o RuntimeException, si la quieres no comprobada. Esta es la versión más pequeña:

class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) {
        super(message);
    }
}

void main() {
    IO.println(new InsufficientFundsException("short by 30"));
}

La compilación falla con:

Main.java:1: warning: [serial] serializable class Main.InsufficientFundsException has no definition of serialVersionUID
error: warnings found and -Werror specified

Throwable implementa Serializable, así que toda excepción es serializable, y -Xlint:all le pide a cada clase serializable que declare un número de versión para su forma serializada. Un simple javac no dice nada, pero agregar el campo cuesta una línea. Fíjate también en el nombre de la advertencia: Main.InsufficientFundsException. En un archivo fuente compacto, cada clase que declaras vive dentro de una clase oculta Main.

Una buena excepción propia lleva como campos los datos que necesita quien la maneja, no solo dentro del texto del mensaje:

class InsufficientFundsException extends Exception {
    private static final long serialVersionUID = 1L;

    final String account;
    final long shortByCents;

    InsufficientFundsException(String account, long shortByCents) {
        super("account " + account + " is short by " + shortByCents + " cents");
        this.account = account;
        this.shortByCents = shortByCents;
    }
}

class Account {
    final String id;
    long balanceCents;

    Account(String id, long balanceCents) {
        this.id = id;
        this.balanceCents = balanceCents;
    }

    void withdraw(long cents) throws InsufficientFundsException {
        if (cents > balanceCents) {
            throw new InsufficientFundsException(id, cents - balanceCents);
        }
        balanceCents -= cents;
    }
}

void main() {
    var account = new Account("ACC-7", 1_000);
    try {
        account.withdraw(400);
        account.withdraw(900);
    } catch (InsufficientFundsException e) {
        IO.println(e.getMessage());
        IO.println("offer a top-up of " + e.shortByCents + " cents to " + e.account);
    }
    IO.println("balance: " + account.balanceCents);
    IO.println(new InsufficientFundsException("X", 1));
}

Imprime:

account ACC-7 is short by 300 cents
offer a top-up of 300 cents to ACC-7
balance: 600
Main$InsufficientFundsException: account X is short by 1 cents

El manejador lee e.shortByCents directamente. No tiene que volver a sacar un número de un string. El segundo retiro lanzó la excepción antes de que se ejecutara balanceCents -= cents, así que el saldo se quedó en 600.

La última línea vuelve a mostrar la clase oculta. El toString de una excepción usa el nombre binario de la clase, Main$InsufficientFundsException, y una excepción no capturada imprime ese nombre en su stack trace. En un proyecto con archivos reales, sería el nombre simple de la clase.

Quedarse sin dinero es algo que quien llama puede manejar, así que esta es comprobada. Para una regla rota dentro de tu propio código, extiende RuntimeException, y quienes llaman no necesitan throws. En los dos casos, agrega un constructor que reciba un Throwable cause si la excepción alguna vez va a envolver a otra.

try-with-resources cierra las cosas por ti

Una sentencia try-with-resources declara recursos entre paréntesis después de try, y cierra cada uno cuando el bloque termina, termine como termine. Un recurso es cualquier objeto que implementa AutoCloseable, que tiene un solo método, close(). Los archivos, los sockets y las conexiones a bases de datos lo implementan. Para que la salida sea predecible, este post usa una clase pequeña que imprime cuando se abre y cuando se cierra:

class Res implements AutoCloseable {
    final String name;

    Res(String name) {
        this.name = name;
        IO.println("open " + name);
    }

    @Override
    public void close() {
        IO.println("close " + name);
    }
}

void main() {
    try (var a = new Res("A"); var b = new Res("B")) {
        IO.println("using " + a.name + " and " + b.name);
    }
    IO.println("after the try");
}

Imprime:

open A
open B
using A and B
close B
close A
after the try

Los recursos se abren en el orden en que los declaras y se cierran en el orden inverso. Eso importa cuando uno depende de otro. Un writer con buffer envuelve un archivo, así que el writer tiene que vaciarse y cerrarse antes de que se cierre el archivo que tiene debajo.

Mientras escribíamos este ejemplo con -Xlint:all aparecieron dos pequeñas sorpresas:

  • Un recurso que nunca usas genera una advertencia. Con el cuerpo vacío, javac informa auto-closeable resource a is never referenced in body of corresponding try statement. Si de verdad solo necesitas la apertura y el cierre, llámalo _, como en try (var _ = new Res("A")), y la advertencia desaparece.
  • close() debe declarar solo lo que lanza. AutoCloseable.close() está declarado como throws Exception. Cuando Res.close() conservaba esa cláusula, javac avisaba que could throw InterruptedException. La solución es no declarar nada, como arriba, o un tipo específico como IOException.

El cuerpo lanza una excepción, y los recursos igual se cierran

Un recurso se cierra incluso cuando el cuerpo lanza una excepción, y se cierra antes de que se ejecute cualquier catch de la misma sentencia:

class Res implements AutoCloseable {
    final String name;

    Res(String name) {
        this.name = name;
        IO.println("open " + name);
    }

    @Override
    public void close() {
        IO.println("close " + name);
    }
}

void main() {
    try (var a = new Res("A"); var b = new Res("B")) {
        IO.println("using " + a.name + " and " + b.name);
        throw new IllegalStateException("disk full");
    } catch (IllegalStateException e) {
        IO.println("caught: " + e.getMessage());
    }
}

Imprime:

open A
open B
using A and B
close B
close A
caught: disk full

Los dos recursos se cerraron antes de que se imprimiera caught: disk full. Las variables a y b ni siquiera están en alcance dentro del catch, y eso es a propósito: cuando el catch se ejecuta, los recursos ya están cerrados.

Mira cómo se cierran los recursos

El orden se ve mejor paso a paso. La animación sigue el programa de arriba, y su salida se va llenando a la derecha:

try (var a = new Res("A"); var b = new Res("B")) { IO.println("using " + a.name + " and " + b.name); throw new IllegalStateException("disk full"); } catch (IllegalStateException e) { recursos A abre cierra 2.º B abre cierra 1.º catch (IllegalStateException e) IllegalStateException salida open A open B using A and B close B close A caught: disk full empieza la sentencia try: aún no hay nada abierto new Res("A") va primero: A se abre new Res("B") va después: B se abre el cuerpo usa A y B, y lanza IllegalStateException antes de salir la excepción, cierra B: último abierto, primero cerrado luego cierra A, porque se abrió antes que B ambos están cerrados y la excepción llega al bloque catch

try-with-resources abre A y luego B. El cuerpo lanza una excepción. Antes de que la excepción pueda salir del bloque try, se cierra B, luego se cierra A, y solo entonces el bloque catch recibe la excepción.

Aquí están esos pasos en palabras, por si la animación no se reproduce:

  1. La sentencia try empieza, y todavía no hay nada abierto.
  2. new Res("A") se ejecuta, imprime open A y se convierte en el recurso a.
  3. new Res("B") se ejecuta, imprime open B y se convierte en el recurso b.
  4. El cuerpo imprime using A and B y luego lanza IllegalStateException.
  5. Antes de que la excepción salga del try, Java cierra los recursos, empezando por el último que se abrió. b.close() imprime close B.
  6. a.close() imprime close A.
  7. Con los dos cerrados, la excepción llega a catch (IllegalStateException e), que imprime caught: disk full.

Cuando close() también lanza: excepciones suprimidas

Si el cuerpo lanza una excepción y un close() también lanza una, gana la excepción del cuerpo, y los fallos al cerrar se le adjuntan como excepciones suprimidas (suppressed). Las lees con getSuppressed():

class Res implements AutoCloseable {
    final String name;

    Res(String name) {
        this.name = name;
        IO.println("open " + name);
    }

    @Override
    public void close() {
        IO.println("close " + name);
        throw new IllegalStateException("close failed: " + name);
    }
}

void main() {
    try (var a = new Res("A"); var b = new Res("B")) {
        IO.println("using " + a.name + " and " + b.name);
        throw new RuntimeException("body failed");
    } catch (RuntimeException e) {
        IO.println("caught: " + e.getMessage());
        for (Throwable s : e.getSuppressed()) {
            IO.println("suppressed: " + s.getMessage());
        }
    }
}

Imprime:

open A
open B
using A and B
close B
close A
caught: body failed
suppressed: close failed: B
suppressed: close failed: A

El close() de B lanzó una excepción, y Java igual llamó al de A. No se perdió nada. La excepción del cuerpo suele ser el problema real, así que es la que capturas, y los fallos al cerrar van con ella en el orden en que ocurrieron. Una excepción no capturada los imprime en su stack trace bajo Suppressed:.

Compáralo con la forma antigua, una llamada a close() dentro de finally. Ahí, una excepción de close() reemplaza la excepción del cuerpo, y el problema original desaparece, igual que pasó con return en finally.

Cerrar una variable que ya tienes

Desde Java 9, los paréntesis pueden nombrar una variable existente en vez de declarar una nueva, siempre que sea final o efectivamente final, es decir, que nunca se reasigne:

class Res implements AutoCloseable {
    final String name;

    Res(String name) {
        this.name = name;
        IO.println("open " + name);
    }

    @Override
    public void close() {
        IO.println("close " + name);
    }
}

Res openLog() {
    return new Res("log");
}

void main() {
    Res log = openLog();
    try (log; var _ = new Res("lock")) {
        IO.println("writing to " + log.name);
    }
}

Imprime:

open log
open lock
writing to log
close lock
close log

Comprobamos la versión con una clase clásica: javac --release 8 rechaza try (r) con variables in try-with-resources are not supported in -source 8, y --release 9 lo acepta. Asigna log una segunda vez y javac se niega con variable log used as a try-with-resources resource neither final nor effectively final. La regla asegura que el objeto que se cierra es el que querías cerrar.

Tres hábitos que conviene evitar

La mayoría de los bugs con excepciones vienen de unos pocos hábitos, y el peor de ellos esconde un bug real detrás de un catch que no dice nada. Este bucle suma precios y se salta los que no puede leer:

record Item(String name, String price) {}

int total(List<Item> items) {
    int sum = 0;
    for (var item : items) {
        try {
            sum += Integer.parseInt(item.price().strip());
        } catch (Exception e) {
            // skip prices we can't read
        }
    }
    return sum;
}

void main() {
    var items = new ArrayList<Item>();
    items.add(new Item("tea", "3"));
    items.add(new Item("cake", "4 euros"));
    items.add(new Item("coffee", null));
    items.add(new Item("water", "2"));
    IO.println("total: " + total(items));
}

Imprime:

total: 5

"4 euros" lanzó NumberFormatException, que es para lo que se escribió el catch. Pero el precio del café es null, así que item.price().strip() lanzó NullPointerException, un bug distinto, y catch (Exception e) también se lo tragó. javac no avisó del bloque vacío, ni siquiera con -Xlint:all. Son tres errores en uno:

  • Un bloque catch vacío. Como mínimo, registra lo que te saltaste. Si de verdad quieres ignorar una excepción, explica por qué en un comentario y llama a la variable _, como en catch (NumberFormatException _), para que el lector sepa que es a propósito.
  • Capturar Exception. Captura el tipo que esperas, aquí NumberFormatException. Así el null haría fallar el programa de forma visible en la primera ejecución, y alguien arreglaría los datos.
  • Usar excepciones para el flujo de control normal. Si los precios incorrectos son una entrada normal, revísalos primero con un if. Una excepción debería significar que pasó algo inesperado, y además construir un stack trace para cada fila es trabajo extra.

Qué recordar

  • Todo lo que se lanza extiende Throwable. Error es para problemas de la JVM que no capturas, RuntimeException y sus subclases son no comprobadas, y todas las demás Exception son comprobadas.
  • El compilador te obliga a capturar o declarar una excepción comprobada. Usa no comprobadas para errores de programación y comprobadas para fallos de los que quien llama se puede recuperar.
  • Ordena los bloques catch del más específico al más amplio. finally siempre se ejecuta, y un return dentro de él se traga las excepciones sin decir nada.
  • Lanza con un mensaje que nombre la regla y el valor incorrecto, y pasa la excepción original como causa cuando la envuelvas.
  • Dale a una excepción propia un serialVersionUID y campos para los datos que necesita quien la maneja.
  • try-with-resources cierra los recursos en orden inverso, antes de que se ejecute catch, incluso cuando el cuerpo lanza una excepción. Los fallos de close() se convierten en excepciones suprimidas.
  • No dejes un bloque catch vacío, y no captures Exception para silenciar un problema que no has nombrado.

Captura lo que puedas manejar, deja pasar lo que no, y deja que try-with-resources se encargue de cerrar.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.