Semestre 02, 2026
Una excepción es una situación anormal que ocurre durante la ejecución del programa e interrumpe su flujo normal.
nombres.get(100) cuando solamente existen tres elementos.El programa lanza un error y puede detenerse abruptamente.
Al enfrentarnos a una excepción solo existen tres salidas posibles.
Throwable
├─ Error (fallos graves: no capturar)
│ ├─ OutOfMemoryError
│ └─ StackOverflowError
└─ Exception
├─ RuntimeException (no chequeadas)
│ ├─ NullPointerException
│ └─ ArithmeticException
├─ IOException (chequeadas)
└─ SQLException
Verificadas por el compilador. Obligan a usar try/catch o declarar throws. Representan condiciones recuperables, como leer un archivo que podría no existir.
Heredan de RuntimeException. Suelen indicar errores de programación o mal uso de una API. El compilador no obliga a capturarlas.
IOException, SQLException.NullPointerException, IllegalArgumentException.| Excepción | Cuándo ocurre |
|---|---|
NullPointerException | Usamos un objeto que vale null |
ArithmeticException | División entera entre cero |
NumberFormatException | Convertir a número un texto que no lo es |
IndexOutOfBoundsException | Pedir una posición que no existe |
InputMismatchException | Scanner recibe un tipo distinto al esperado |
IOException | Falla una operación de entrada o salida |
ArrayList<String> nombres = new ArrayList<>();
nombres.add("Ana");
// IndexOutOfBoundsException: solo existe el índice 0
System.out.println(nombres.get(5));
size() lanza IndexOutOfBoundsException.ConcurrentModificationException.Validar con size() o isEmpty() antes de acceder evita la mayoría de estos errores sin necesidad de capturarlos.
try {
int numero = Integer.parseInt("hola");
} catch (NumberFormatException e) {
System.out.println("Debe ingresar un número.");
}
Debe ingresar un número.
Scanner teclado = new Scanner(System.in);
try {
System.out.print("Ingrese su edad: ");
String entrada = teclado.nextLine();
int edad = Integer.parseInt(entrada);
System.out.println("Edad: " + edad);
} catch (NumberFormatException e) {
System.out.println("Error: ingrese un número entero.");
}
Scanner teclado = new Scanner(System.in);
try {
System.out.print("Ingrese su edad: ");
String entrada = teclado.nextLine();
int edad = Integer.parseInt(entrada);
System.out.println("Edad: " + edad);
} catch (NumberFormatException e) {
System.out.println("Error: ingrese un número entero.");
}
Creamos el objeto que leerá lo que la persona escriba en el teclado.
Scanner teclado = new Scanner(System.in);
try {
System.out.print("Ingrese su edad: ");
String entrada = teclado.nextLine();
int edad = Integer.parseInt(entrada);
System.out.println("Edad: " + edad);
} catch (NumberFormatException e) {
System.out.println("Error: ingrese un número entero.");
}
Dentro del try colocamos la parte riesgosa: pedimos el dato y lo recibimos como texto, sin saber todavía si es un número.
Scanner teclado = new Scanner(System.in);
try {
System.out.print("Ingrese su edad: ");
String entrada = teclado.nextLine();
int edad = Integer.parseInt(entrada);
System.out.println("Edad: " + edad);
} catch (NumberFormatException e) {
System.out.println("Error: ingrese un número entero.");
}
Esta es la línea que puede fallar. Si el texto no representa un entero, Java lanza una NumberFormatException y el flujo salta al catch.
Scanner teclado = new Scanner(System.in);
try {
System.out.print("Ingrese su edad: ");
String entrada = teclado.nextLine();
int edad = Integer.parseInt(entrada);
System.out.println("Edad: " + edad);
} catch (NumberFormatException e) {
System.out.println("Error: ingrese un número entero.");
}
Si la persona escribe veinte, el programa no se cae: muestra el mensaje Error: ingrese un número entero y continúa su ejecución.
try {
Files.readString(Path.of("data.txt"));
} catch (FileNotFoundException e) {
System.out.println("Archivo no encontrado");
} catch (IOException e) {
System.out.println("Error de entrada o salida");
}
Los catch se escriben del más específico al más general. Si el general va primero, atrapa todo y el compilador marca el resto como inalcanzable.
Cuando varias excepciones se manejan igual, se agrupan en un solo catch.
try {
Files.readString(Path.of("data.txt"));
} catch (NoSuchFileException | AccessDeniedException e) {
System.out.println("Archivo inaccesible");
} catch (IOException e) {
e.printStackTrace();
}
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
getMessage() devuelve la descripción del error: / by zeroprintStackTrace() muestra el rastro de llamadas hasta la línea que falló.
try {
System.out.println("Procesando...");
} catch (Exception e) {
System.out.println("Ocurrió un error.");
} finally {
System.out.println("Proceso finalizado.");
}
Un archivo abierto debe cerrarse siempre, haya funcionado la lectura o no.
FileReader fr = null;
try {
fr = new FileReader("data.txt");
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
} finally {
if (fr != null) {
fr.close();
}
}
Declarar la variable afuera, comprobar que no sea null y cerrarla a mano: tres pasos que es fácil olvidar.
Las dos versiones abren el mismo archivo y lo cierran igual de bien.
A mano, con finally
FileReader fr = null;
try {
fr = new FileReader("data.txt");
} catch (IOException e) {
System.out.println("Error");
} finally {
if (fr != null) {
fr.close();
}
}
Automático, con try de recursos
try (FileReader fr = new FileReader("data.txt")) {
// Java cierra fr al salir
} catch (IOException e) {
System.out.println("Error");
}
La versión automática elimina la variable externa, la comprobación de null y el bloque finally completo.
El recurso se declara entre los paréntesis que van justo después de la palabra try.
try (FileReader fr = new FileReader("data.txt")) {
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
}
try (FileReader fr = new FileReader("data.txt")) {
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
}
Dentro de los paréntesis va una declaración de variable normal, la misma que antes escribíamos afuera. Colocarla ahí es la señal para Java de que ese objeto es un recurso que hay que cerrar.
try (FileReader fr = new FileReader("data.txt")) {
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
}
Dentro del bloque usamos el recurso como cualquier otra variable. Solo existe aquí adentro: fuera del try la variable fr ya no está disponible.
try (FileReader fr = new FileReader("data.txt")) {
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
}
Al llegar a esta llave Java llama solo a fr.close(), ocurra o no una excepción. Es exactamente lo que hacía el finally, pero escrito por el lenguaje en lugar de por nosotros.
try (FileReader fr = new FileReader("data.txt")) {
System.out.println(fr.read());
} catch (IOException e) {
System.out.println("Error de lectura");
}
El catch se escribe igual que siempre y sigue atrapando los errores de lectura. Lo único que desaparece del código es el bloque finally.
Cuando trabajamos con archivos, conexiones o cualquier objeto que se cierre. Es la forma recomendada.
Cuando la tarea final no es cerrar un recurso: mostrar un mensaje, restablecer una bandera o liberar algo que Java no sabe cerrar.
Solo pueden ir entre los paréntesis los objetos que Java sabe cerrar, es decir los que implementan la interfaz AutoCloseable.
La palabra clave throw genera una excepción al detectar una condición inválida.
public int dividir(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("b != 0");
}
return a / b;
}
Si b vale cero, el método se interrumpe en esa línea y la excepción viaja hacia quien lo invocó.
La cláusula throws se escribe en la firma del método, antes de abrir la llave.
public void cargar() throws IOException {
Files.readString(Path.of("config.json"));
}
El throws es un letrero de peligro en la puerta del método: avisa que llamar a cargar puede salir mal. Quien lo lea sabe que ese bloque de código es problemático antes de siquiera ejecutarlo.
Al ver el letrero, quien llama a cargar elige entre resolver el problema o volver a pasarlo.
Opción A: capturarla y resolverla aquí
public void iniciar() {
try {
cargar();
} catch (IOException e) {
System.out.println("No se pudo cargar");
}
}
Opción B: volver a declarar el throws
public void iniciar() throws IOException {
cargar();
}
En la opción B el método iniciar no maneja nada: copia el mismo letrero en su propia firma y le pasa el problema a quien lo llamó a él.
Mientras nadie la capture, la excepción sigue subiendo por los métodos que llamaron.
Ninguna de estas dos clases resuelve el problema: las dos solo lo anuncian.
// Clase 1: aquí ocurre el problema
public class Lector {
public String leer(String ruta) throws IOException {
return Files.readString(Path.of(ruta));
}
}
// Clase 2: no lo resuelve, repite el letrero
public class Configuracion {
private Lector lector = new Lector();
public String cargar() throws IOException {
return lector.leer("config.json");
}
}
// Clase 3: aquí sí se decide qué hacer
public class Main {
public static void main(String[] args) {
Configuracion config = new Configuracion();
try {
System.out.println(config.cargar());
} catch (IOException e) {
System.out.println("No se pudo leer la configuración");
}
}
}
El error nació en Lector, atravesó Configuracion sin que nadie lo tocara y se resolvió hasta Main. Main es la única clase que escribe un try.
Extiende Exception para una chequeada, o RuntimeException para una no chequeada.
public class SaldoInsuficienteException extends RuntimeException {
public SaldoInsuficienteException(String mensaje) {
super(mensaje);
}
}
public class OperacionBancariaException extends Exception {
private final String cuenta;
private final double monto;
public OperacionBancariaException(String cta, double monto, String msg) {
super(msg);
this.cuenta = cta;
this.monto = monto;
}
public String getCuenta() { return cuenta; }
public double getMonto() { return monto; }
}
public void retirar(double monto) {
if (monto > saldo) {
throw new SaldoInsuficienteException("Saldo insuficiente");
}
saldo -= monto;
}
El nombre de la excepción comunica el problema del dominio mucho mejor que un mensaje genérico de error.
Escribir código semántico es usar elementos que explican su propio significado y propósito por sí mismos, en lugar de contenedores vacíos o nombres confusos.
Contenedor vacío
try {
cuenta.retirar(500);
} catch (Exception e) {
System.out.println("Error");
}
Nombre que se explica solo
try {
cuenta.retirar(500);
} catch (SaldoInsuficienteException e) {
System.out.println("Fondos insuficientes");
}
catch vacío.IllegalArgumentException.SaldoInsuficienteException.Exception cuando sea posible.