El manejo de errores y excepciones en Java es uno de los pilares fundamentales para escribir aplicaciones robustas y confiables. Una excepción (exception) es un evento anómalo que interrumpe el flujo normal de ejecución de un programa y que, si no se gestiona adecuadamente, provoca la terminación abrupta de la aplicación.
Java proporciona un mecanismo elegante basado en cinco palabras reservadas —try, catch, finally, throw y throws— que permite separar el código «normal» del código de tratamiento de errores, propagar excepciones por la pila de llamadas y clasificarlas en una jerarquía de clases. En este artículo se estudia todo el sistema de excepciones de Java con ejemplos ejecutables, buenas prácticas y ejercicios resueltos.
❓ ¿Qué es una excepción en Java?
Una excepción es un objeto que encapsula información sobre un error ocurrido durante la ejecución: su tipo (clase), un mensaje descriptivo y la traza de la pila de llamadas (stack trace) en el momento del fallo. Cuando un método detecta una situación excepcional, crea un objeto excepción y lo lanza (throw). El sistema de ejecución de Java (runtime) busca entonces hacia atrás en la pila de llamadas hasta encontrar un manejador adecuado (catch) que capture la excepción.
Si ningún método de la pila captura la excepción, la JVM muestra la traza completa en la salida de error estándar y termina el hilo (o el programa, si es el hilo principal).
🌳 Jerarquía de excepciones en Java
Todas las excepciones y errores en Java son subclases de java.lang.Throwable. La jerarquía se divide en dos ramas principales:
| Clase | Tipo | Descripción | Ejemplos |
|---|---|---|---|
Error | Errores graves | Problemas graves del entorno de ejecución que no deben capturarse. | OutOfMemoryError, StackOverflowError |
Exception | Excepciones checked | Errores recuperables. El compilador obliga a manejarlas. | IOException, SQLException |
RuntimeException | Excepciones unchecked | Errores de programación. No requieren manejo obligatorio. | NullPointerException, ArithmeticException |
Diagrama simplificado de la jerarquía
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error (NO capturar)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── java.lang.Exception (CHECKED - capturar o declarar)
├── IOException
│ └── FileNotFoundException
├── SQLException
└── java.lang.RuntimeException (UNCHECKED - no obligatorio)
├── NullPointerException
├── ArithmeticException
├── ArrayIndexOutOfBoundsException
├── IllegalArgumentException
└── ...
⚖️ Excepciones checked vs unchecked
Esta distinción es una de las características más importantes del sistema de excepciones de Java.
| Característica | Checked | Unchecked |
|---|---|---|
| Hereda de | Exception (no RuntimeException) | RuntimeException |
| Verificación | En tiempo de compilación | En tiempo de ejecución |
| Obligación | Hay que capturarla o declararla con throws | No hay obligación |
| Causa típica | Condiciones externas: fichero no encontrado, conexión caída | Errores de programación: índice fuera de rango |
| Ejemplo | IOException, SQLException | NullPointerException, ArithmeticException |
🛡️ Bloques try, catch y finally
El mecanismo básico de manejo de excepciones se compone de tres bloques:
| Bloque | Obligatorio | Se ejecuta cuando... |
|---|---|---|
try | Sí | Siempre. Contiene el código que puede lanzar excepciones. |
catch | Al menos uno (o un finally) | Solo si se lanza una excepción del tipo especificado. |
finally | No | Siempre, haya o no excepción. Ideal para liberar recursos. |
Sintaxis básica
try {
int resultado = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Error aritmético: " + e.getMessage());
} catch (Exception e) {
System.out.println("Error genérico: " + e.getMessage());
} finally {
System.out.println("Limpieza completada");
}
// Salida:
// Error aritmético: / by zero
// Limpieza completada
Múltiples bloques catch
Podemos encadenar varios bloques catch para manejar distintos tipos de excepción. Es fundamental ordenarlos de más específico a más general.
public class MultipleCatch {
public static void main(String[] args) {
int[] numeros = {10, 20, 30};
try {
int valor = numeros[5]; // ArrayIndexOutOfBoundsException
int resultado = valor / 0; // ArithmeticException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Índice fuera de rango: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Error aritmético: " + e.getMessage());
} catch (Exception e) {
System.out.println("Otro error: " + e.getMessage());
}
}
}
// Salida: Índice fuera de rango: Index 5 out of bounds for length 3
🎯 throw y throws: lanzar y declarar excepciones
Es importante no confundir estas dos palabras reservadas:
| Aspecto | throw | throws |
|---|---|---|
| Ubicación | Dentro del cuerpo de un método | En la firma del método |
| Función | Lanza una excepción explícitamente | Declara que el método puede propagar una excepción |
| Sintaxis | throw new Exception("msg"); | void metodo() throws Exception { } |
| Cantidad | Lanza una sola excepción cada vez | Puede declarar varias, separadas por comas |
Ejemplo: validación con throw y throws
public class Validador {
public static void validarEdad(int edad) throws IllegalArgumentException {
if (edad < 0 || edad > 150) {
throw new IllegalArgumentException(
"Edad no válida: " + edad + ". Debe estar entre 0 y 150.");
}
System.out.println("Edad " + edad + " validada correctamente.");
}
public static void main(String[] args) {
try {
validarEdad(25); // OK
validarEdad(-5); // Lanza excepción
validarEdad(30); // No se ejecuta
} catch (IllegalArgumentException e) {
System.out.println("Error de validación: " + e.getMessage());
}
}
}
// Salida:
// Edad 25 validada correctamente.
// Error de validación: Edad no válida: -5. Debe estar entre 0 y 150.
🔀 Multi-catch y try-with-resources
Desde Java 7, el lenguaje incorpora dos mejoras significativas: la captura múltiple (multi-catch) y la gestión automática de recursos (try-with-resources).
Multi-catch (Java 7+)
try {
String texto = null;
int longitud = texto.length(); // NullPointerException
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
System.out.println("Error: " + e.getClass().getSimpleName() + " - " + e.getMessage());
}
// Salida: Error: NullPointerException - null
Try-with-resources (Java 7+)
Cierra automáticamente los recursos que implementan AutoCloseable al finalizar el bloque.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LeerArchivo {
public static void main(String[] args) {
try (BufferedReader lector = new BufferedReader(new FileReader("datos.txt"))) {
String linea;
while ((linea = lector.readLine()) != null) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error al leer archivo: " + e.getMessage());
}
// No es necesario lector.close() en un finally
}
}
FileReader, Connection, Scanner), utilizar try-with-resources.
🔧 Crear excepciones personalizadas
Cuando las excepciones estándar de Java no describen el error del dominio de la aplicación, podemos crear nuestras propias clases extendiendo Exception (checked) o RuntimeException (unchecked).
// Excepción checked personalizada
class SaldoInsuficienteException extends Exception {
private double saldoActual;
private double cantidadSolicitada;
public SaldoInsuficienteException(double saldo, double cantidad) {
super("Saldo insuficiente. Disponible: " + saldo + " €, solicitado: " + cantidad + " €");
this.saldoActual = saldo;
this.cantidadSolicitada = cantidad;
}
public double getSaldoActual() { return saldoActual; }
public double getCantidadSolicitada() { return cantidadSolicitada; }
}
class CuentaBancaria {
private String titular;
private double saldo;
public CuentaBancaria(String titular, double saldoInicial) {
this.titular = titular;
this.saldo = saldoInicial;
}
public void retirar(double cantidad) throws SaldoInsuficienteException {
if (cantidad > saldo) {
throw new SaldoInsuficienteException(saldo, cantidad);
}
saldo -= cantidad;
System.out.println("Retirada de " + cantidad + " €. Saldo: " + saldo + " €");
}
}
public class EjemploExcepcionPersonalizada {
public static void main(String[] args) {
CuentaBancaria cuenta = new CuentaBancaria("Ana García", 1000.0);
try {
cuenta.retirar(300.0); // OK
cuenta.retirar(900.0); // Lanza SaldoInsuficienteException
} catch (SaldoInsuficienteException e) {
System.out.println("Error: " + e.getMessage());
System.out.println("Faltan: " + (e.getCantidadSolicitada() - e.getSaldoActual()) + " €");
}
}
}
// Salida:
// Retirada de 300.0 €. Saldo: 700.0 €
// Error: Saldo insuficiente. Disponible: 700.0 €, solicitado: 900.0 €
// Faltan: 200.0 €
✅ Buenas prácticas en el manejo de excepciones
| Práctica | Descripción |
|---|---|
| Capturar excepciones específicas | Usar catch (IOException e) en lugar de catch (Exception e). |
| No silenciar excepciones | Un catch vacío es un antipatrón grave. Al mínimo, registrar el error. |
| Usar try-with-resources | Para ficheros, conexiones, streams. Evita fugas de recursos. |
| Mensajes descriptivos | Incluir contexto útil: throw new IOException("No se pudo abrir: " + ruta). |
| No usar excepciones para control de flujo | Las excepciones son para situaciones excepcionales, no para lógica habitual. |
| Documentar con Javadoc | Documentar con @throws en el Javadoc del método. |
| Preferir unchecked para errores de programación | Usar IllegalArgumentException, IllegalStateException, etc. |
Antipatrón vs buena práctica
try {
conexion.ejecutarConsulta(sql);
} catch (SQLException e) {
// ❌ NUNCA: el error se pierde completamente
}
try {
conexion.ejecutarConsulta(sql);
} catch (SQLException e) {
System.err.println("Error en consulta: " + sql);
e.printStackTrace();
throw new PersistenciaException("Fallo en consulta", e);
}
🏗️ Ejemplo completo integrador
El siguiente programa simula un sistema de pedidos online que combina todos los conceptos: excepciones personalizadas, try-catch-finally, throw/throws, multi-catch y buenas prácticas.
// Excepción checked: error de negocio
class ProductoNoDisponibleException extends Exception {
public ProductoNoDisponibleException(String producto) {
super("Producto agotado: " + producto);
}
}
// Excepción unchecked: error de programación
class PedidoInvalidoException extends RuntimeException {
public PedidoInvalidoException(String motivo) {
super("Pedido inválido: " + motivo);
}
}
class Inventario {
private String[] productos = {"Portátil", "Teclado", "Ratón", "Monitor"};
private int[] stock = {5, 10, 0, 3};
public void verificarDisponibilidad(String producto, int cantidad)
throws ProductoNoDisponibleException {
for (int i = 0; i < productos.length; i++) {
if (productos[i].equalsIgnoreCase(producto)) {
if (stock[i] < cantidad) {
throw new ProductoNoDisponibleException(
producto + " (disponible: " + stock[i]
+ ", solicitado: " + cantidad + ")");
}
stock[i] -= cantidad;
System.out.println("✓ Reservado: " + cantidad + "x " + producto);
return;
}
}
throw new ProductoNoDisponibleException(producto + " (no existe en catálogo)");
}
}
class Pedido {
private String cliente;
private String producto;
private int cantidad;
private double precioUnitario;
public Pedido(String cliente, String producto, int cantidad, double precio) {
if (cliente == null || cliente.isEmpty()) {
throw new PedidoInvalidoException("El nombre del cliente no puede estar vacío");
}
if (cantidad <= 0) {
throw new PedidoInvalidoException("La cantidad debe ser mayor que 0");
}
this.cliente = cliente;
this.producto = producto;
this.cantidad = cantidad;
this.precioUnitario = precio;
}
public String getProducto() { return producto; }
public int getCantidad() { return cantidad; }
public double getTotal() { return cantidad * precioUnitario; }
@Override
public String toString() {
return String.format("Pedido[%s, %dx %s, Total: %.2f €]",
cliente, cantidad, producto, getTotal());
}
}
public class SistemaPedidos {
public static void main(String[] args) {
Inventario inventario = new Inventario();
Object[][] datosPedidos = {
{"Ana", "Teclado", 2, 29.99},
{"", "Portátil", 1, 899.0}, // Cliente vacío
{"Carlos", "Ratón", 3, 15.50}, // Sin stock
{"David", "Monitor", 1, 249.99},
{"Elena", "Tablet", 1, 199.0}, // No existe
};
int procesados = 0, fallidos = 0;
for (Object[] datos : datosPedidos) {
System.out.println("\n--- Procesando pedido ---");
try {
Pedido pedido = new Pedido(
(String) datos[0], (String) datos[1],
(int) datos[2], (double) datos[3]);
System.out.println("Pedido creado: " + pedido);
inventario.verificarDisponibilidad(
pedido.getProducto(), pedido.getCantidad());
System.out.println("✅ Pedido confirmado: "
+ String.format("%.2f", pedido.getTotal()) + " €");
procesados++;
} catch (PedidoInvalidoException | ProductoNoDisponibleException e) {
System.out.println("❌ " + e.getMessage());
fallidos++;
} finally {
System.out.println(" [Registro de auditoría actualizado]");
}
}
System.out.println("\n=== RESUMEN ===");
System.out.println("Procesados: " + procesados);
System.out.println("Fallidos: " + fallidos);
}
}
// Salida:
// --- Procesando pedido ---
// Pedido creado: Pedido[Ana, 2x Teclado, Total: 59.98 €]
// ✓ Reservado: 2x Teclado
// ✅ Pedido confirmado: 59.98 €
// [Registro de auditoría actualizado]
// --- Procesando pedido ---
// ❌ Pedido inválido: El nombre del cliente no puede estar vacío
// [Registro de auditoría actualizado]
// --- Procesando pedido ---
// Pedido creado: Pedido[Carlos, 3x Ratón, Total: 46.50 €]
// ❌ Producto agotado: Ratón (disponible: 0, solicitado: 3)
// [Registro de auditoría actualizado]
// --- Procesando pedido ---
// Pedido creado: Pedido[David, 1x Monitor, Total: 249.99 €]
// ✓ Reservado: 1x Monitor
// ✅ Pedido confirmado: 249.99 €
// [Registro de auditoría actualizado]
// --- Procesando pedido ---
// Pedido creado: Pedido[Elena, 1x Tablet, Total: 199.00 €]
// ❌ Producto agotado: Tablet (no existe en catálogo)
// [Registro de auditoría actualizado]
// === RESUMEN ===
// Procesados: 2
// Fallidos: 3
⚠️ Errores frecuentes
Error 1: Catch vacío que silencia el error
try {
int resultado = Integer.parseInt(texto);
} catch (NumberFormatException e) {
// ❌ El error desaparece sin dejar rastro
}
try {
int resultado = Integer.parseInt(texto);
} catch (NumberFormatException e) {
System.err.println("Formato no válido: '" + texto + "' - " + e.getMessage());
}
Error 2: Capturar Exception genérico demasiado pronto
try { leerArchivo(); procesarDatos(); }
catch (Exception e) { System.out.println("Algo falló"); } // ❌ Captura TODO
try { leerArchivo(); procesarDatos(); }
catch (FileNotFoundException e) { System.out.println("Archivo no encontrado"); }
catch (IOException e) { System.out.println("Error de lectura"); }
Error 3: Confundir throw con throws
public void metodo() {
throws new IOException("Error"); // ❌ throws es para la firma
}
public void metodo() throws IOException {
throw new IOException("Error de E/S"); // ✅ throw lanza, throws declara
}
Error 4: Ordenar catches de general a específico
try { leerArchivo(); }
catch (Exception e) { } // ❌ Más general primero
catch (IOException e) { } // ❌ Inalcanzable
try { leerArchivo(); }
catch (IOException e) { } // ✅ Más específico primero
catch (Exception e) { } // ✅ Más general después
📝 Ejercicios prácticos
Ejercicio 1: ¿Qué imprime este código? (comprensión)
public class Ejercicio1 {
public static void main(String[] args) {
System.out.println("A");
try {
System.out.println("B");
int x = 10 / 0;
System.out.println("C");
} catch (ArithmeticException e) {
System.out.println("D");
} finally {
System.out.println("E");
}
System.out.println("F");
}
}
Ejercicio 2: Conversor seguro de texto a número (aplicación)
Escribe un método static int convertirSeguro(String texto, int valorPorDefecto) que intente convertir una cadena a entero. Si falla, devuelve el valor por defecto. Prueba con: "42", "abc", null, "".
Ejercicio 3: Sistema de registro con excepciones personalizadas (diseño)
Crea una excepción checked RegistroException con código de error y un método registrar(String nombre, String email, int edad) que valide: nombre no vacío (E001), email con @ (E002), edad 18-120 (E003). Si falla, lanza RegistroException.
❓ Preguntas frecuentes sobre Manejo de errores y excepciones en Java: guía completa con ejemplos
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Manejo de errores y excepciones en Java: guía completa con ejemplos? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!