Saltar al contenido
excepciones.java · devschool

Excepciones en Java

Lección 20 de 26 · 12 min de lectura · Actualizado el

En esta lección
  1. Qué es una excepción
  2. La jerarquía de excepciones
  3. Checked y unchecked
  4. try y catch: capturar una excepción
  5. finally: código que se ejecuta siempre
  6. try-with-resources: cerrar recursos automáticamente
  7. throw y throws: lanzar excepciones
  8. Propagar excepciones
  9. Crear tus propias excepciones
  10. Leer un stack trace
  11. Buenas prácticas y errores frecuentes
  12. Resumen

Un programa real se encuentra con situaciones que no controla: un fichero que no existe, un usuario que escribe “abc” donde se esperaba un número o una división entre cero. En Java, esos problemas se comunican con excepciones. En esta lección aprenderás qué son, cómo capturarlas para que el programa no se detenga, cómo lanzar las tuyas y cómo leer el famoso stack trace que aparece en la consola cuando algo falla.

Qué es una excepción

Una excepción es un objeto que Java crea cuando ocurre un error durante la ejecución. Ese objeto describe qué ha pasado (el tipo de error y un mensaje) y dónde (la cadena de métodos que se estaban ejecutando).

Cuando se produce una excepción, Java interrumpe el método actual y la “lanza” hacia arriba, al método que lo llamó. Si nadie la captura, llega hasta main y el programa termina con un mensaje de error:

int[] notas = {7, 9, 5};
System.out.println(notas[3]);
System.out.println("Fin");
// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
//     Index 3 out of bounds for length 3

La línea "Fin" nunca se imprime: el array solo tiene las posiciones 0, 1 y 2 (lo viste en arrays).

¿Por qué excepciones y no, por ejemplo, devolver -1 cuando algo falla? Porque un código de error se ignora sin darse cuenta. Una excepción no se puede ignorar: o la tratas o el programa se detiene y te avisa.

La jerarquía de excepciones

Las excepciones son clases, y como aprendiste en herencia, forman una jerarquía. Esta es la parte que más vas a usar:

Throwable
├── Error                          (fallos graves de la JVM: no se capturan)
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception                      (problemas que el programa puede tratar)
    ├── IOException                (checked)
    │   └── FileNotFoundException
    ├── SQLException               (checked)
    └── RuntimeException           (unchecked)
        ├── ArithmeticException
        ├── NullPointerException
        ├── IllegalArgumentException
        │   └── NumberFormatException
        ├── IndexOutOfBoundsException
        │   └── ArrayIndexOutOfBoundsException
        └── ClassCastException
  • Throwable es la raíz: todo lo que se puede lanzar hereda de ella.
  • Error representa problemas de la propia máquina virtual, como quedarse sin memoria o una recursividad infinita (StackOverflowError). No se capturan.
  • Exception agrupa los problemas que tu código sí puede y debe tratar.
  • RuntimeException es una rama especial de Exception: son errores de programación, como acceder a un índice que no existe.

Gracias al polimorfismo, un catch (Exception e) captura también todas sus subclases.

Checked y unchecked

Java divide las excepciones en dos grupos, y la diferencia es muy importante:

Checked (comprobadas)Unchecked (no comprobadas)
ClasesException y sus hijas, salvo RuntimeExceptionRuntimeException, Error y sus hijas
EjemplosIOException, SQLException, FileNotFoundExceptionNullPointerException, ArithmeticException, NumberFormatException
¿Obliga el compilador a tratarlas?Sí: capturar con catch o declarar con throwsNo
Origen típicoAlgo externo falla (disco, red, base de datos)Un fallo en tu código (un null, un índice mal calculado)

Si llamas a un método que lanza una excepción checked y no la tratas, no compila:

String texto = Files.readString(Path.of("datos.txt"));
// error: unreported exception IOException; must be caught or declared to be thrown

El compilador te obliga a pensar qué hacer si el fichero no existe. Con las unchecked no: no deberían ocurrir si el código es correcto.

try y catch: capturar una excepción

Para tratar una excepción, se mete el código que puede fallar en un bloque try y se indica en catch qué hacer si falla:

try {
    int[] notas = {7, 9, 5};
    System.out.println(notas[3]);
    System.out.println("Esto no se imprime");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Error: " + e.getMessage());
}
System.out.println("El programa sigue");
// Error: Index 3 out of bounds for length 3
// El programa sigue

Cómo funciona:

  1. Java ejecuta el try línea a línea.
  2. En cuanto una línea lanza una excepción, salta directamente al catch que coincide con su tipo. El resto del try se descarta.
  3. Después del catch, el programa continúa normalmente.
  4. Si no hay ninguna excepción, el catch se ignora.

La variable e es el objeto excepción. Sus métodos más útiles:

try {
    Integer.parseInt("12a");
} catch (NumberFormatException e) {
    System.out.println(e.getMessage()); // For input string: "12a"
    System.out.println(e);              // java.lang.NumberFormatException: For input string: "12a"
}

e.printStackTrace() imprime además la traza completa, muy útil mientras depuras.

Varios catch

Un mismo try puede tener varios catch, uno por tipo de error. Java prueba de arriba abajo y entra en el primero que encaje:

String[] entradas = {"42", "abc", null};
for (String t : entradas) {
    try {
        int n = Integer.parseInt(t.trim());
        System.out.println("Número: " + n);
    } catch (NumberFormatException e) {
        System.out.println("No es un número: " + t);
    } catch (NullPointerException e) {
        System.out.println("No hay texto");
    }
}
// Número: 42
// No es un número: abc
// No hay texto

Cuidado: pon siempre las excepciones más concretas primero. Si pones catch (Exception e) antes que catch (NumberFormatException e), el segundo nunca se alcanzaría y el compilador da el error exception NumberFormatException has already been caught.

Multi-catch

Cuando dos errores se tratan igual, se pueden unir con | en un solo catch:

String[] cantidades = {"10", "0", "x"};
for (String c : cantidades) {
    try {
        int porPersona = 100 / Integer.parseInt(c);
        System.out.println("Por persona: " + porPersona);
    } catch (NumberFormatException | ArithmeticException e) {
        System.out.println("Dato no válido: " + e.getClass().getSimpleName());
    }
}
// Por persona: 10
// Dato no válido: ArithmeticException
// Dato no válido: NumberFormatException

finally: código que se ejecuta siempre

El bloque finally va tras los catch y se ejecuta pase lo que pase: haya excepción o no, se capture o no, e incluso si el try hace return. Se usa para liberar recursos o dejar las cosas en orden:

try {
    System.out.println("Abriendo caja");
    int r = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("No se puede dividir entre cero");
} finally {
    System.out.println("Cerrando caja");
}
// Abriendo caja
// No se puede dividir entre cero
// Cerrando caja

try-with-resources: cerrar recursos automáticamente

Ficheros, conexiones a bases de datos o conexiones de red son recursos que hay que cerrar al terminar. Antes se cerraban en un finally, lo que daba un código largo y propenso a olvidos. Desde Java 7 existe el try-with-resources: declaras el recurso entre paréntesis tras el try y Java lo cierra solo al salir del bloque, falle o no:

static String leerPrimeraLinea(String ruta) throws IOException {
    try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
        return lector.readLine();
    } // aquí se llama a lector.close() automáticamente
}

Funciona con cualquier clase que implemente la interfaz AutoCloseable. Se pueden abrir varios recursos separados por ; y se cierran en orden inverso:

class Conexion implements AutoCloseable {
    private final String nombre;
    Conexion(String nombre) {
        this.nombre = nombre;
        System.out.println("Abriendo " + nombre);
    }
    @Override
    public void close() {
        System.out.println("Cerrando " + nombre);
    }
}

try (Conexion a = new Conexion("A"); Conexion b = new Conexion("B")) {
    System.out.println("Trabajando");
}
// Abriendo A
// Abriendo B
// Trabajando
// Cerrando B
// Cerrando A

Lo usarás constantemente en ficheros y en JDBC.

throw y throws: lanzar excepciones

Hasta ahora las excepciones las lanzaba Java. Tú también puedes lanzarlas con throw cuando detectas algo incorrecto. Es la forma correcta de validar los datos que recibe un método:

public void setEdad(int edad) {
    if (edad < 0) {
        throw new IllegalArgumentException("La edad no puede ser negativa: " + edad);
    }
    this.edad = edad;
}

throw crea y lanza el objeto; el método se interrumpe en ese punto, como con un return.

throws (con s) es otra cosa: va en la cabecera del método y avisa de que ese método puede lanzar una excepción checked que no trata él mismo:

public static String cargarConfiguracion() throws IOException {
    return Files.readString(Path.of("config.txt"));
}
PalabraDónde vaQué hace
throwDentro del métodoLanza una excepción concreta: throw new ...
throwsEn la cabeceraDeclara qué excepciones puede lanzar el método

Propagar excepciones

Cuando un método no sabe qué hacer con un error, lo mejor es dejarlo pasar hacia arriba (propagarlo) con throws, hasta llegar a quien sí sabe tratarlo, normalmente la parte que habla con el usuario:

static void cargarDatos() throws IOException {
    leerPrimeraLinea("no-existe.txt");   // no la captura: la propaga
}

public static void main(String[] args) {
    try {
        cargarDatos();
    } catch (IOException e) {
        System.out.println("No se pudo leer: " + e.getMessage());
    }
}
// No se pudo leer: no-existe.txt (No such file or directory)

A veces conviene envolver una excepción en otra más significativa para tu programa. Pasa la original como segundo argumento para no perder la información (se llama causa):

try {
    Integer.parseInt(cantidad);
} catch (NumberFormatException e) {
    throw new IllegalStateException("Pedido con cantidad incorrecta", e);
}
// Quien la capture puede ver e.getCause():
// java.lang.NumberFormatException: For input string: "abc"

Crear tus propias excepciones

Para errores propios de tu aplicación puedes crear una clase que herede de Exception (checked) o de RuntimeException (unchecked). Por convención, su nombre termina en Exception:

public class SaldoInsuficienteException extends Exception {
    private final double falta;

    public SaldoInsuficienteException(String mensaje, double falta) {
        super(mensaje);          // guarda el mensaje para getMessage()
        this.falta = falta;
    }

    public double getFalta() {
        return falta;
    }
}

La usamos en una cuenta bancaria:

public class Cuenta {
    private double saldo;

    public Cuenta(double saldo) { this.saldo = saldo; }

    public void retirar(double importe) throws SaldoInsuficienteException {
        if (importe <= 0) {
            throw new IllegalArgumentException("El importe debe ser positivo: " + importe);
        }
        if (importe > saldo) {
            throw new SaldoInsuficienteException("Saldo insuficiente", importe - saldo);
        }
        saldo -= importe;
    }

    public double getSaldo() { return saldo; }
}
Cuenta cuenta = new Cuenta(100);
try {
    cuenta.retirar(30);
    cuenta.retirar(120);
} catch (SaldoInsuficienteException e) {
    System.out.println(e.getMessage() + ". Faltan " + e.getFalta() + " €");
}
System.out.println("Saldo: " + cuenta.getSaldo());
// Saldo insuficiente. Faltan 50.0 €
// Saldo: 70.0

¿Checked o unchecked? Una regla práctica: si quien llama puede hacer algo razonable para recuperarse (pedir otro importe, avisar al usuario), usa checked. Si el error indica un fallo de programación (un importe negativo que nunca debería llegar), usa unchecked, como hemos hecho con IllegalArgumentException.

Leer un stack trace

Cuando una excepción no se captura, Java imprime la traza de la pila (stack trace). Aprender a leerla te ahorrará horas. Mira este programa:

public class Tienda {
    public static void main(String[] args) {
        int[] ventas = {};
        mostrarInforme(ventas);
    }
    static void mostrarInforme(int[] ventas) {
        System.out.println("Media: " + calcularMedia(ventas));
    }
    static int calcularMedia(int[] ventas) {
        int suma = 0;
        for (int v : ventas) suma += v;
        return suma / ventas.length;
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Tienda.calcularMedia(Tienda.java:12)
	at Tienda.mostrarInforme(Tienda.java:7)
	at Tienda.main(Tienda.java:4)

Se lee así:

  1. Primera línea: el tipo de excepción (ArithmeticException) y el mensaje (/ by zero). Ya sabes qué ha pasado.
  2. Primera línea at: dónde se produjo exactamente: método calcularMedia, archivo Tienda.java, línea 12. Empieza a buscar ahí.
  3. Resto de líneas: el camino que llevó hasta allí, de más reciente a más antiguo: main llamó a mostrarInforme, que llamó a calcularMedia.

Si aparece Caused by: más abajo, es la excepción original que se envolvió; suele ser la más reveladora.

Consejo: en proyectos grandes la traza incluye líneas de bibliotecas (java.base/...). Busca la primera línea que pertenezca a tu código: casi siempre el error está ahí.

Buenas prácticas y errores frecuentes

  • No dejes un catch vacío. catch (Exception e) { } se “traga” el error y el programa sigue con datos incorrectos sin que nadie se entere. Como mínimo, muestra un mensaje o regístralo.
  • No captures Exception por costumbre. Captura el tipo concreto que esperas. Un catch (Exception e) genérico esconde también fallos de programación que deberías corregir.
  • No uses excepciones para el flujo normal. Si puedes comprobar algo con un if (que el divisor no sea cero), hazlo. Y un NullPointerException se arregla corrigiendo el código, no rodeándolo de try.
  • Cierra los recursos con try-with-resources, no a mano.
  • Da mensajes claros al lanzar (incluye el dato que falló) y conserva la causa al envolver: new MiException("...", e).

Resumen

ConceptoPara qué sirve
try { }Código que puede fallar
catch (Tipo e) { }Qué hacer si falla con ese tipo de excepción
catch (A | B e)Tratar varios tipos igual (multi-catch)
finally { }Código que se ejecuta siempre
try (Recurso r = ...) { }Cierra el recurso automáticamente
throw new X("...")Lanzar una excepción
throws XDeclarar que un método puede lanzarla
CheckedEl compilador obliga a tratarla (IOException, SQLException)
UncheckedHereda de RuntimeException; errores de programación
Excepción propiaclass MiException extends Exception
e.getMessage(), e.getCause()Mensaje y excepción original

En la siguiente lección dejarás atrás los arrays de tamaño fijo y aprenderás a usar listas que crecen solas: ArrayList y listas.

Pon a prueba lo que has aprendido

[Java] ¿Qué imprime este código?
try {
    System.out.print("A ");
    int x = 5 / 0;
    System.out.print("B ");
} catch (ArithmeticException e) {
    System.out.print("C ");
} finally {
    System.out.print("D ");
}
System.out.println("E");

[Java] ¿Cuál de estas excepciones es checked, es decir, el compilador obliga a capturarla o declararla con throws?

[Java] ¿Por qué no compila este código?
try {
    int n = Integer.parseInt(texto);
} catch (Exception e) {
    System.out.println("Error general");
} catch (NumberFormatException e) {
    System.out.println("No es un número");
}

[Java] ¿Qué diferencia hay entre throw y throws?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.