</>waridocu

Java

Excepciones: try, catch y throw

Qué pasa cuando un programa se rompe, cómo atraparlo con try/catch, cuándo lanzar una excepción propia con throw y la diferencia entre excepciones checked y unchecked.

javaexcepcionestrycatchthrowerrores

El problema: el programa se corta a la mitad

Este programa pide un número y lo duplica:

Ejemplo: funciona perfecto... hasta que el usuario escribe "hola"java
Scanner teclado = new Scanner(System.in);
System.out.print("Escribe un número: ");
int n = Integer.parseInt(teclado.nextLine());
System.out.println("El doble es " + (n * 2));
System.out.println("Gracias por usar el programa.");

Si el usuario escribe hola, la salida es:

texttext
Escribe un número: hola
Exception in thread "main" java.lang.NumberFormatException: For input string: "hola"
	at java.base/java.lang.Integer.parseInt(Integer.java:652)
	at Main.main(Main.java:5)

Ni el doble ni el “Gracias”: el programa murió en la línea 5 y las siguientes nunca se ejecutaron. Eso es una excepción sin atrapar.

Una excepción es un objeto que Java crea cuando algo sale mal, y que “lanza” hacia arriba interrumpiendo la ejecución normal. Si nadie la atrapa, sube hasta main, el programa termina, y lo que ves impreso es el stack trace: la lista de métodos por los que pasó, del más profundo al más superficial. En el ejemplo se lee de abajo hacia arriba — Main.main línea 5 llamó a Integer.parseInt, y ahí explotó.

try / catch: atrapar el problema

try se traduce como “intentar” y catch como “atrapar”, y hacen exactamente eso:

Ejemplo: el mismo programa, que ahora sobrevivejava
Scanner teclado = new Scanner(System.in);
System.out.print("Escribe un número: ");

try {
    int n = Integer.parseInt(teclado.nextLine());
    System.out.println("El doble es " + (n * 2));
} catch (NumberFormatException e) {
    System.out.println("Eso no era un número.");
}

System.out.println("Gracias por usar el programa.");
texttext
Escribe un número: hola
Eso no era un número.
Gracias por usar el programa.

Cómo se lee:

  • try { ... } encierra el código que puede fallar. Si algo lanza una excepción ahí adentro, el resto del bloque se saltea inmediatamente.
  • catch (NumberFormatException e) { ... } dice “si lo que se lanzó es de este tipo, ejecuta esto”. e es el objeto excepción, con información sobre lo que pasó.
  • Después del catch, el programa sigue normalmente. Esa es la diferencia con el ejemplo anterior.
Importante

El catch atrapa solo el tipo que declara (y sus subclases). Si dentro del try ocurre una excepción de otro tipo, este catch no se activa y el programa muere igual. Atrapar es una decisión deliberada sobre qué problema esperas y sabes manejar.

Varios catch

Un mismo try puede tener varios catch, y se evalúan en orden hasta que uno coincide:

Ejemplo: dos fallas distintas, dos respuestas distintasjava
try {
    int[] notas = {8, 6, 9};
    int indice = Integer.parseInt(teclado.nextLine());
    System.out.println(notas[indice]);
} catch (NumberFormatException e) {
    System.out.println("Eso no era un número.");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Ese índice no existe: solo hay 3 notas.");
}
Advertencia

Ordena los catch de lo más específico a lo más general. Como toda excepción hereda de Exception, poner catch (Exception e) primero atraparía todo y dejaría a los siguientes inalcanzables — el compilador directamente lo rechaza con exception has already been caught.

finally: pase lo que pase

finally se traduce como “finalmente”, y su bloque se ejecuta siempre: haya fallado o no, se haya atrapado la excepción o no, incluso si hubo un return dentro del try.

Ejemplo: cerrar el recurso ocurra lo que ocurrajava
Scanner teclado = new Scanner(System.in);
try {
    int n = Integer.parseInt(teclado.nextLine());
    System.out.println(100 / n);
} catch (ArithmeticException e) {
    System.out.println("No se puede dividir por cero.");
} finally {
    teclado.close();
    System.out.println("Listo.");
}

Su razón de ser es liberar cosas que quedarían colgando: archivos abiertos, conexiones a bases de datos, Scanner. Si el cierre estuviera solo al final del try, una excepción se lo saltearía.

Qué trae el objeto excepción

e no es decorativo. Los métodos que más vas a usar:

MétodoQué devuelve
e.getMessage()El texto que describe el problema (For input string: "hola")
e.printStackTrace()Imprime la traza completa, como si nadie la hubiera atrapado
e.toString()Nombre de la clase de la excepción + mensaje
javajava
catch (NumberFormatException e) {
    System.out.println("Entrada inválida: " + e.getMessage());
}
Advertencia

El anti-patrón más dañino de todo Java es el catch vacío:

javajava
try { ... } catch (Exception e) { }

El programa no muere, pero el error desaparece sin dejar rastro y el bug reaparece más adelante, sin ninguna pista de dónde nació. Si de verdad no hay nada que hacer con la excepción, deja al menos un e.printStackTrace() o un mensaje.

Las excepciones que más vas a ver

ExcepciónCuándo se lanza
NullPointerExceptionUsar un método o atributo sobre una variable que vale null
ArrayIndexOutOfBoundsExceptionPedir notas[5] en un array de 3
NumberFormatExceptionInteger.parseInt("hola")
ArithmeticExceptionDivisión entera por cero (10 / 0)
InputMismatchExceptionteclado.nextInt() cuando el usuario escribió texto
ClassCastExceptionUn cast a un tipo que el objeto no tiene

Todas heredan de Exception, que a su vez hereda de Throwable. Es la herencia de siempre, y es lo que hace que catch (Exception e) pueda atraparlas a todas.

throw: lanzar una excepción propia

Hasta acá atrapaste excepciones ajenas. También puedes lanzarlas: es la forma correcta de que un método diga “esto que me pediste no tiene sentido” sin devolver un valor falso ni fallar en silencio.

Vuelve al setter de Encapsulación, que rechazaba las notas inválidas callándose:

Ejemplo: validar avisando, en vez de validar en silenciojava
public void setNota(double nuevaNota) {
    if (nuevaNota < 0 || nuevaNota > 10) {
        throw new IllegalArgumentException("La nota debe estar entre 0 y 10, llegó: " + nuevaNota);
    }
    this.nota = nuevaNota;
}

throw (“lanzar”) crea la excepción con new y corta el método ahí mismo. Quien llamó decide qué hacer:

javajava
try {
    alumno.setNota(-3);
} catch (IllegalArgumentException e) {
    System.out.println("No se pudo cargar la nota: " + e.getMessage());
}
texttext
No se pudo cargar la nota: La nota debe estar entre 0 y 10, llegó: -3.0

Compara con la versión que solo hacía if (...) { this.nota = ... }: ahí el valor inválido se descartaba sin que nadie se enterara, y el objeto quedaba con un 0.0 que nadie pidió. Con throw, el problema es imposible de ignorar.

Nota

IllegalArgumentException (“argumento ilegal”) es la excepción estándar para “me pasaste un valor que no acepto”. Para “el objeto no está en un estado que permita esto” existe IllegalStateException. Usar las de la biblioteca estándar antes de inventar una propia hace el código más fácil de leer para cualquiera.

Excepciones propias

Cuando el dominio del programa tiene un error con nombre propio, se crea una clase que herede de Exception:

Ejemplo: una excepción con el vocabulario del problemajava
public class SaldoInsuficienteException extends Exception {
    public SaldoInsuficienteException(String mensaje) {
        super(mensaje);
    }
}
javajava
public void retirar(double monto) throws SaldoInsuficienteException {
    if (monto > saldo) {
        throw new SaldoInsuficienteException("Faltan $" + (monto - saldo));
    }
    saldo -= monto;
}

Toda la clase es un constructor que le pasa el mensaje al padre con super(mensaje) — el resto lo hereda de Exception.

throws: declarar que un método puede fallar

Fíjate en la firma de arriba: public void retirar(double monto) throws SaldoInsuficienteException. throws (con s, distinto de throw) no lanza nada: avisa que este método podría lanzar esa excepción, y obliga a quien lo llame a hacerse cargo.

Eso lleva a la última distinción, la que explica por qué unas excepciones te obligan a escribir try y otras no:

Unchecked (no verificadas)Checked (verificadas)
Heredan deRuntimeExceptionException (pero no de RuntimeException)
¿El compilador te obliga a atraparlas?No
EjemplosNullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsIOException, SQLException, y las propias como la de arriba
Qué suelen representarUn bug en el códigoUna condición externa previsible (no hay red, falta el archivo)

Por eso Integer.parseInt("hola") compila sin try (es unchecked), pero abrir un archivo no: IOException es checked y el compilador exige atraparla o declararla con throws.

Las dos formas válidas de responder a una excepción checkedjava
public void procesar() {
    try {
        cuenta.retirar(1000);
    } catch (SaldoInsuficienteException e) {
        System.out.println(e.getMessage());
    }
}
O pasarle el problema a quien llame a este métodojava
public void procesar() throws SaldoInsuficienteException {
    cuenta.retirar(1000);
}
Importante

Que una excepción sea unchecked no significa que no debas manejarla: significa que el compilador no te obliga. NumberFormatException al leer del teclado es enteramente previsible, y atraparla es exactamente lo correcto.

Pon a prueba

🎯 ¿Qué imprime este programa?
javajava
try {
    System.out.println("A");
    int x = 10 / 0;
    System.out.println("B");
} catch (ArithmeticException e) {
    System.out.println("C");
} finally {
    System.out.println("D");
}
  • Correcto. A se imprime antes de fallar. La división por cero lanza ArithmeticException, así que B se saltea y salta directo al catch, que imprime C. El finally imprime D siempre. Y después del bloque el programa sigue vivo, porque la excepción fue atrapada.

  • Sería así si la excepción se lanzara después de imprimir B. Pero 10 / 0 está en la línea anterior: al lanzarse la excepción, todo el resto del try se abandona sin ejecutarse.

  • Falta D. El bloque finally es precisamente el que se ejecuta pase lo que pase — con excepción, sin excepción, e incluso si hubiera un return dentro del try.

  • Sería así si no hubiera catch, o si el catch declarara un tipo que no coincide. Aquí ArithmeticException es exactamente lo que se lanzó, así que queda atrapada.

💡 Lee el stack trace

Un programa muere con esto. ¿Qué salió mal, en qué línea está el problema, y cuál es la corrección?

texttext
Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "String.length()" because "<local1>" is null
    at Main.contarLetras(Main.java:12)
    at Main.main(Main.java:5)
javajava
public class Main {
    public static void main(String[] args) {
        String[] nombres = new String[3];
        nombres[0] = "Ana";
        System.out.println(contarLetras(nombres[1]));
    }

    static int contarLetras(String texto) {
        return texto.length();
    }
}

Qué pasó: new String[3] reserva tres casillas pero no crea ningún String; cada una arranca en null. Solo se llenó nombres[0], así que nombres[1] sigue valiendo null. Ese null llega a contarLetras como parámetro y texto.length() intenta llamar un método sobre la nada.

Dónde está el problema: el stack trace se lee de abajo hacia arriba. La línea 5 (main) es la que hizo la llamada, y la línea 12 (contarLetras) es donde reventó. La causa está en main — pasar un elemento nunca inicializado — aunque el síntoma aparezca en contarLetras. Ese salto entre “dónde explota” y “quién lo causó” es lo que hace que leer la traza completa valga la pena.

Cómo se corrige: lo más directo es que el método se defienda de la entrada inválida:

javajava
static int contarLetras(String texto) {
    if (texto == null) {
        return 0;
    }
    return texto.length();
}

Si en cambio recibir null es un error de quien llama y no algo que deba tolerarse, la alternativa correcta es lanzar en vez de disimular:

javajava
if (texto == null) {
    throw new IllegalArgumentException("contarLetras no acepta null");
}
✏️ Completa el manejo de errores

El método debe rechazar edades negativas lanzando una excepción, y main debe atraparla mostrando su mensaje.

public static void setEdad(int edad) {
  if (edad < 0) {
       new IllegalArgumentException("Edad negativa: " + edad);
  }
}

public static void main(String[] args) {
   {
      setEdad(-5);
  }  (IllegalArgumentException e) {
      System.out.println(e.());
  }
}

🧩 Ordena el código

Ordena las líneas para leer un número del teclado atrapando la entrada inválida y cerrando el Scanner siempre.

  1. System.out.println("Eso no era un número.");
  2. Scanner teclado = new Scanner(System.in);
  3. } catch (NumberFormatException e) {
  4. int n = Integer.parseInt(teclado.nextLine());
  5. }
  6. System.out.println("El doble es " + (n * 2));
  7. teclado.close();
  8. } finally {
  9. try {

Siguiente paso

Ya cubriste el lenguaje base completo: tipos, control de flujo, métodos, colecciones, orientación a objetos y errores. Para consultar de un vistazo qué significa cada símbolo en cualquier código Java que leas, está el Repaso de sintaxis.