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.
El problema: el programa se corta a la mitad
Este programa pide un número y lo duplica:
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:
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:
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.");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”.ees 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.
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:
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.");
}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.
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étodo | Qué 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 |
catch (NumberFormatException e) {
System.out.println("Entrada inválida: " + e.getMessage());
}El anti-patrón más dañino de todo Java es el catch vacío:
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ón | Cuándo se lanza |
|---|---|
NullPointerException | Usar un método o atributo sobre una variable que vale null |
ArrayIndexOutOfBoundsException | Pedir notas[5] en un array de 3 |
NumberFormatException | Integer.parseInt("hola") |
ArithmeticException | División entera por cero (10 / 0) |
InputMismatchException | teclado.nextInt() cuando el usuario escribió texto |
ClassCastException | Un 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:
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:
try {
alumno.setNota(-3);
} catch (IllegalArgumentException e) {
System.out.println("No se pudo cargar la nota: " + e.getMessage());
}No se pudo cargar la nota: La nota debe estar entre 0 y 10, llegó: -3.0Compara 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.
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:
public class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(String mensaje) {
super(mensaje);
}
}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 de | RuntimeException | Exception (pero no de RuntimeException) |
| ¿El compilador te obliga a atraparlas? | No | Sí |
| Ejemplos | NullPointerException, IllegalArgumentException, ArrayIndexOutOfBounds | IOException, SQLException, y las propias como la de arriba |
| Qué suelen representar | Un bug en el código | Una 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.
public void procesar() {
try {
cuenta.retirar(1000);
} catch (SaldoInsuficienteException e) {
System.out.println(e.getMessage());
}
}public void procesar() throws SaldoInsuficienteException {
cuenta.retirar(1000);
}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
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");
}Un programa muere con esto. ¿Qué salió mal, en qué línea está el problema, y cuál es la corrección?
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)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:
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:
if (texto == null) {
throw new IllegalArgumentException("contarLetras no acepta null");
}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 las líneas para leer un número del teclado atrapando la entrada inválida y cerrando el Scanner siempre.
- System.out.println("Eso no era un número.");
- Scanner teclado = new Scanner(System.in);
- } catch (NumberFormatException e) {
- int n = Integer.parseInt(teclado.nextLine());
- }
- System.out.println("El doble es " + (n * 2));
- teclado.close();
- } finally {
- 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.