Cuando escribes un programa, imaginas que todo va a salir bien: el usuario ingresa datos correctos, los archivos existen, la red funciona y la base de datos responde. Pero en la realidad, las cosas fallan. El usuario ingresa letras donde se esperaban números, el archivo que buscas no existe, la conexión se cae a mitad de una operación.
Java tiene un mecanismo robusto y elegante para manejar esas situaciones inesperadas: las excepciones. Aprenderlas bien es uno de los pasos más importantes para escribir código profesional, estable y fácil de mantener.
En este artículo aprenderás qué es una excepción, cómo funciona el mecanismo de try/catch, la jerarquía de excepciones en Java, cómo crear tus propias excepciones y las buenas prácticas que usan los desarrolladores experimentados.
¿Qué es una excepción?
Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de instrucciones. Cuando algo sale mal, Java crea un objeto que representa ese error, lo lanza hacia arriba por la pila de llamadas y, si nadie lo captura, el programa termina mostrando un mensaje de error.
Por ejemplo, si intentas dividir un número entre cero:
public class Ejemplo {
public static void main(String[] args) {
int resultado = 10 / 0; // lanza ArithmeticException
System.out.println(resultado); // esta línea nunca se ejecuta
}
}Java lanzará esto en consola:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Ejemplo.main(Ejemplo.java:3)El programa se detiene. Sin manejo de excepciones, un solo error inesperado puede tumbar toda la aplicación.
La jerarquía de excepciones en Java
En Java, todas las excepciones son objetos que heredan de la clase Throwable. De ahí se dividen en dos grandes ramas:
- Error: problemas graves del sistema que normalmente no deberías intentar capturar, como quedarse sin memoria (
OutOfMemoryError) o un desbordamiento de pila (StackOverflowError). Indican que algo fundamental falló. - Exception: situaciones anómalas que sí puedes y debes manejar en tu código.
Dentro de Exception, hay otra división crucial:
- Checked exceptions (verificadas): el compilador te obliga a manejarlas o declararlas. Son situaciones que el programa debería anticipar, como no encontrar un archivo (
FileNotFoundException) o un problema de red (IOException). - Unchecked exceptions (no verificadas): heredan de
RuntimeException. El compilador no te obliga a manejarlas. Suelen representar errores de programación, como acceder a un índice inválido (ArrayIndexOutOfBoundsException) o usar una referencia nula (NullPointerException).
Throwable
├── Error
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── IOException (checked)
├── SQLException (checked)
├── FileNotFoundException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArithmeticException
├── ArrayIndexOutOfBoundsException
├── ClassCastException
└── NumberFormatExceptionEl bloque try/catch: capturar excepciones
La estructura básica para manejar excepciones en Java es el bloque try/catch. El código que puede fallar va dentro de try, y el código que maneja el error va dentro de catch.
try {
// código que puede lanzar una excepción
} catch (TipoDeExcepcion e) {
// código que maneja el error
}Aplicado al ejemplo de la división:
public class Division {
public static void main(String[] args) {
try {
int resultado = 10 / 0;
System.out.println("Resultado: " + resultado);
} catch (ArithmeticException e) {
System.out.println("Error: no se puede dividir entre cero.");
System.out.println("Mensaje: " + e.getMessage());
}
System.out.println("El programa continúa después del error.");
}
}Salida:
Error: no se puede dividir entre cero.
Mensaje: / by zero
El programa continúa después del error.El programa ya no se cae. El error se captura, se maneja y la ejecución continúa normalmente después del bloque try/catch.
El objeto de excepción: qué información contiene
La variable e dentro del catch es el objeto de excepción. Contiene información valiosa para entender qué salió mal:
try {
int[] numeros = {1, 2, 3};
System.out.println(numeros[10]); // índice inválido
} catch (ArrayIndexOutOfBoundsException e) {
// Mensaje de error
System.out.println("Mensaje: " + e.getMessage());
// Nombre de la clase de excepción
System.out.println("Tipo: " + e.getClass().getName());
// Traza completa del error (útil para depuración)
e.printStackTrace();
}e.printStackTrace() imprime en consola toda la cadena de llamadas que llevó al error, lo cual es invaluable durante el desarrollo para entender exactamente dónde y por qué ocurrió el problema.
Capturar múltiples excepciones
Un bloque try puede lanzar distintos tipos de excepciones. Puedes capturarlas con bloques catch separados, uno por tipo:
import java.util.Scanner;
public class Calculadora {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
System.out.print("Ingresa el dividendo: ");
int dividendo = Integer.parseInt(scanner.nextLine());
System.out.print("Ingresa el divisor: ");
int divisor = Integer.parseInt(scanner.nextLine());
int resultado = dividendo / divisor;
System.out.println("Resultado: " + resultado);
} catch (NumberFormatException e) {
System.out.println("Error: debes ingresar números enteros válidos.");
} catch (ArithmeticException e) {
System.out.println("Error: no se puede dividir entre cero.");
}
scanner.close();
}
}Java evalúa los bloques catch de arriba hacia abajo y ejecuta el primero cuyo tipo coincida con la excepción lanzada. Los demás se omiten.
Capturar múltiples tipos en un solo catch
Desde Java 7, puedes capturar varios tipos de excepción en un solo bloque catch usando el operador |:
try {
// código que puede fallar de distintas formas
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Error de formato o aritmético: " + e.getMessage());
}Esto es útil cuando el manejo de error es el mismo para varios tipos de excepción y evita duplicar código.
El bloque finally: código que siempre se ejecuta
finally es un bloque opcional que se ejecuta siempre, sin importar si ocurrió una excepción o no, e incluso si el bloque catch relanza la excepción. Es ideal para liberar recursos como conexiones de base de datos, archivos abiertos o conexiones de red.
public class EjemploFinally {
public static void main(String[] args) {
System.out.println("Inicio");
try {
System.out.println("Dentro del try");
int resultado = 10 / 0; // lanza excepción
System.out.println("Esta línea no se ejecuta");
} catch (ArithmeticException e) {
System.out.println("Dentro del catch: " + e.getMessage());
} finally {
System.out.println("Dentro del finally: siempre se ejecuta");
}
System.out.println("Fin");
}
}Salida:
Inicio
Dentro del try
Dentro del catch: / by zero
Dentro del finally: siempre se ejecuta
FinTry-with-resources: cierre automático de recursos
Desde Java 7, existe una forma más elegante de manejar recursos que deben cerrarse después de usarse: el try-with-resources. Cualquier objeto que implemente la interfaz AutoCloseable declarado en el paréntesis del try se cierra automáticamente al salir del bloque, con o sin error.
// Sin try-with-resources (forma antigua)
BufferedReader lector = null;
try {
lector = new BufferedReader(new FileReader("archivo.txt"));
String linea = lector.readLine();
System.out.println(linea);
} catch (IOException e) {
System.out.println("Error al leer el archivo: " + e.getMessage());
} finally {
if (lector != null) {
try {
lector.close(); // hay que cerrar manualmente
} catch (IOException e) {
e.printStackTrace();
}
}
}// Con try-with-resources (forma moderna y recomendada)
try (BufferedReader lector = new BufferedReader(new FileReader("archivo.txt"))) {
String linea = lector.readLine();
System.out.println(linea);
} catch (IOException e) {
System.out.println("Error al leer el archivo: " + e.getMessage());
}
// lector.close() se llama automáticamente al salir del bloqueEl código es mucho más limpio y seguro. Puedes declarar múltiples recursos separándolos con punto y coma:
try (
FileReader archivo = new FileReader("entrada.txt");
BufferedReader lector = new BufferedReader(archivo)
) {
String linea;
while ((linea = lector.readLine()) != null) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}Lanzar excepciones con throw
No solo puedes capturar excepciones: también puedes lanzarlas tú mismo cuando detectas una situación inválida en tu código usando la palabra clave throw.
public class CuentaBancaria {
private double saldo;
public CuentaBancaria(double saldoInicial) {
if (saldoInicial < 0) {
throw new IllegalArgumentException("El saldo inicial no puede ser negativo.");
}
this.saldo = saldoInicial;
}
public void retirar(double monto) {
if (monto <= 0) {
throw new IllegalArgumentException("El monto a retirar debe ser mayor que cero.");
}
if (monto > saldo) {
throw new IllegalStateException("Saldo insuficiente. Saldo actual: " + saldo);
}
saldo -= monto;
System.out.println("Retiro exitoso. Saldo restante: " + saldo);
}
public static void main(String[] args) {
try {
CuentaBancaria cuenta = new CuentaBancaria(1000);
cuenta.retirar(500);
cuenta.retirar(800); // lanzará IllegalStateException
} catch (IllegalArgumentException | IllegalStateException e) {
System.out.println("Error en la operación: " + e.getMessage());
}
}
}Declarar excepciones con throws
Cuando un método puede lanzar una checked exception pero no la maneja internamente, debe declararla en su firma usando throws. Esto le avisa al código que llama al método que debe manejar esa excepción.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LectorArchivo {
// Este método declara que puede lanzar IOException
public String leerPrimeraLinea(String ruta) throws IOException {
try (BufferedReader lector = new BufferedReader(new FileReader(ruta))) {
return lector.readLine();
}
// La IOException se propaga al método que llame a este
}
public static void main(String[] args) {
LectorArchivo lector = new LectorArchivo();
try {
String linea = lector.leerPrimeraLinea("datos.txt");
System.out.println("Primera línea: " + linea);
} catch (IOException e) {
System.out.println("No se pudo leer el archivo: " + e.getMessage());
}
}
}La diferencia entre throw y throws es importante:
throwlanza una excepción en un punto específico del código.throwsdeclara en la firma del método que ese método puede lanzar cierta excepción.
Crear excepciones personalizadas
Java te permite crear tus propias clases de excepción para representar errores específicos de tu dominio. Esto hace que el código sea más expresivo y los mensajes de error más significativos.
Para crear una excepción personalizada, simplemente extiende Exception (checked) o RuntimeException (unchecked):
// Excepción personalizada unchecked
public class SaldoInsuficienteException extends RuntimeException {
private double saldoActual;
private double montoSolicitado;
public SaldoInsuficienteException(double saldoActual, double montoSolicitado) {
super(String.format(
"Saldo insuficiente. Saldo actual: %.2f, monto solicitado: %.2f",
saldoActual, montoSolicitado
));
this.saldoActual = saldoActual;
this.montoSolicitado = montoSolicitado;
}
public double getSaldoActual() {
return saldoActual;
}
public double getMontoSolicitado() {
return montoSolicitado;
}
}// Usando la excepción personalizada
public class CuentaBancaria {
private double saldo;
public CuentaBancaria(double saldo) {
this.saldo = saldo;
}
public void retirar(double monto) {
if (monto > saldo) {
throw new SaldoInsuficienteException(saldo, monto);
}
saldo -= monto;
}
public static void main(String[] args) {
CuentaBancaria cuenta = new CuentaBancaria(500);
try {
cuenta.retirar(800);
} catch (SaldoInsuficienteException e) {
System.out.println(e.getMessage());
System.out.println("Puedes retirar como máximo: " + e.getSaldoActual());
}
}
}Salida:
Saldo insuficiente. Saldo actual: 500.00, monto solicitado: 800.00
Puedes retirar como máximo: 500.0Encadenamiento de excepciones
A veces capturas una excepción de bajo nivel y quieres lanzar una de más alto nivel que tenga más sentido para quien usa tu código, pero sin perder la información del error original. Para eso existe el encadenamiento de excepciones:
public class ServicioUsuario {
public Usuario buscarUsuario(int id) {
try {
// simula una consulta a base de datos
return consultarBaseDeDatos(id);
} catch (SQLException e) {
// Encadena la excepción original como causa
throw new RuntimeException("Error al buscar el usuario con id: " + id, e);
}
}
}
// Al capturar la excepción, se puede acceder a la causa original
try {
servicio.buscarUsuario(42);
} catch (RuntimeException e) {
System.out.println("Error: " + e.getMessage());
System.out.println("Causa original: " + e.getCause().getMessage());
}Excepciones comunes en Java y cuándo ocurren
| Excepción | Cuándo ocurre |
|---|---|
| NullPointerException | Al usar un objeto que es null (llamar métodos, acceder a campos) |
| ArrayIndexOutOfBoundsException | Al acceder a un índice inválido de un array |
| NumberFormatException | Al convertir un String a número y el formato es inválido |
| ClassCastException | Al hacer un cast inválido entre tipos incompatibles |
| ArithmeticException | Al dividir un entero entre cero |
| IllegalArgumentException | Al pasar un argumento inválido a un método |
| IllegalStateException | Al llamar un método en un estado no apropiado del objeto |
| StackOverflowError | Al tener recursión infinita |
| IOException | Al fallar operaciones de entrada/salida (archivos, red) |
| FileNotFoundException | Al intentar abrir un archivo que no existe |
Buenas prácticas en el manejo de excepciones
No captures excepciones genéricas sin razón
// ❌ Incorrecto: captura todo sin distinción, oculta errores reales
try {
procesarDatos();
} catch (Exception e) {
System.out.println("Algo salió mal");
}
// ✅ Correcto: captura el tipo específico que esperas
try {
procesarDatos();
} catch (IOException e) {
System.out.println("Error al leer los datos: " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("Formato de dato inválido: " + e.getMessage());
}Nunca silencies una excepción
// ❌ Pésima práctica: el error ocurre pero nadie lo sabe
try {
operacionPeligrosa();
} catch (Exception e) {
// vacío: silencia el error completamente
}
// ✅ Al menos registra el error
try {
operacionPeligrosa();
} catch (Exception e) {
e.printStackTrace(); // durante desarrollo
// logger.error("Error en operación", e); // en producción
}Lanza excepciones en el nivel correcto de abstracción
// ❌ Incorrecto: expone detalles de implementación
public Usuario buscarUsuario(int id) throws SQLException {
// ...
}
// ✅ Correcto: la excepción tiene sentido para quien usa el método
public Usuario buscarUsuario(int id) throws UsuarioNoEncontradoException {
try {
// consulta a base de datos
} catch (SQLException e) {
throw new UsuarioNoEncontradoException("No existe el usuario: " + id, e);
}
}Usa excepciones para situaciones excepcionales, no para flujo normal
// ❌ Incorrecto: usar excepciones para controlar flujo normal
public boolean existeUsuario(int id) {
try {
buscarUsuario(id);
return true;
} catch (UsuarioNoEncontradoException e) {
return false;
}
}
// ✅ Correcto: verificar antes de actuar
public boolean existeUsuario(int id) {
return repositorio.contarPorId(id) > 0;
}Prefiere excepciones unchecked para errores de programación
Usa checked exceptions para situaciones que el llamador puede anticipar y manejar de forma significativa (archivo no encontrado, error de red). Usa unchecked exceptions para errores de programación que no deberían ocurrir si el código está bien escrito (argumentos nulos, índices inválidos).
Ejemplo completo: sistema de registro de usuarios
Para cerrar, un ejemplo que integra todo lo aprendido:
// Excepciones personalizadas
public class EmailInvalidoException extends RuntimeException {
public EmailInvalidoException(String email) {
super("El email no tiene un formato válido: " + email);
}
}
public class UsuarioDuplicadoException extends RuntimeException {
public UsuarioDuplicadoException(String email) {
super("Ya existe un usuario registrado con el email: " + email);
}
}
// Servicio con manejo de excepciones
import java.util.HashMap;
import java.util.Map;
public class ServicioRegistro {
private Map<String, String> usuarios = new HashMap<>();
public void registrar(String nombre, String email) {
validarNombre(nombre);
validarEmail(email);
verificarDuplicado(email);
usuarios.put(email, nombre);
System.out.println("Usuario registrado: " + nombre + " (" + email + ")");
}
private void validarNombre(String nombre) {
if (nombre == null || nombre.isBlank()) {
throw new IllegalArgumentException("El nombre no puede estar vacío.");
}
}
private void validarEmail(String email) {
if (email == null || !email.contains("@") || !email.contains(".")) {
throw new EmailInvalidoException(email);
}
}
private void verificarDuplicado(String email) {
if (usuarios.containsKey(email)) {
throw new UsuarioDuplicadoException(email);
}
}
public static void main(String[] args) {
ServicioRegistro servicio = new ServicioRegistro();
String[][] intentos = {
{"Ana García", "ana@correo.com"},
{"", "luis@correo.com"},
{"María López", "email-invalido"},
{"Carlos Ruiz", "ana@correo.com"},
{"Pedro Díaz", "pedro@correo.com"},
};
for (String[] intento : intentos) {
try {
servicio.registrar(intento[0], intento[1]);
} catch (IllegalArgumentException e) {
System.out.println("Datos inválidos: " + e.getMessage());
} catch (EmailInvalidoException e) {
System.out.println("Email incorrecto: " + e.getMessage());
} catch (UsuarioDuplicadoException e) {
System.out.println("Duplicado: " + e.getMessage());
}
}
}
}Salida:
Usuario registrado: Ana García (ana@correo.com)
Datos inválidos: El nombre no puede estar vacío.
Email incorrecto: El email no tiene un formato válido: email-invalido
Duplicado: Ya existe un usuario registrado con el email: ana@correo.com
Usuario registrado: Pedro Díaz (pedro@correo.com)Conclusión
El manejo de excepciones es una de las habilidades que distingue a un desarrollador principiante de uno profesional. No se trata solo de evitar que el programa se caiga, sino de anticipar los fallos, comunicarlos de forma clara y mantener el sistema en un estado consistente cuando algo sale mal.
Los puntos más importantes: usa try/catch para capturar errores específicos, usa finally o try-with-resources para liberar recursos, lanza excepciones con throw cuando detectes situaciones inválidas, crea excepciones personalizadas para hacer tu código más expresivo y nunca silencies un error sin al menos registrarlo.
Para seguir avanzando en Java, te recomendamos leer nuestros artículos sobre qué es la programación orientada a objetos y sus 4 pilares y sobre Colecciones en Java: List, Set y Map explicados fácil, que complementan muy bien los conceptos que aprendiste aquí.
No hay comentarios todavía. Sé el primero en compartir tu opinión.