Excepciones en Java: Cuando las cosas salen mal (y cómo no dejar que se cuele algo)
Aprende a manejar excepciones en Java: try-catch, try-with-resources, excepciones personalizadas y mejores prácticas con ejemplos reales.

En los posts anteriores vimos cómo está diseñado Java con POO, las colecciones y cómo pensar algorítmicamente. Pero ahí siempre dejamos algo en el aire: ¿qué pasa cuando algo sale mal?
Porque seamos honestos: en el mundo real, las cosas fallan. El archivo no existe, la red se cae, el usuario escribe "abc" donde debería ir un número, la base de datos está ocupada… Y si no manejamos esos errores bien, nuestra aplicación explota 💥.
Ahí es donde entran las excepciones en Java.
En este post vamos a:
- Entender qué es una excepción (y por qué no es lo mismo que un error).
- Aprender la diferencia entre checked y unchecked exceptions.
- Ver cómo usar
try-catch,try-finallyy try-with-resources. - Crear nuestras propias excepciones personalizadas.
- Y lo más importante: cuándo lanzar excepciones y cuándo solo loggear.
Todo esto con el mismo enfoque de siempre: enunciado → pseudocódigo → test de escritorio → código Java.
Enunciado (el problema)
Estás construyendo un sistema de retiro de efectivo para un cajero automático.
El flujo es:
- El usuario ingresa su número de cuenta (debe ser numérico).
- El sistema busca la cuenta en una base de datos (puede no existir).
- El usuario ingresa el monto a retirar.
- El sistema valida que haya saldo suficiente.
- Se registra la transacción en un archivo de log.
Necesitas manejar estos casos de error:
- El usuario escribe letras donde debería ir un número → entrada inválida
- La cuenta no existe en la base de datos → cuenta no encontrada
- El saldo es insuficiente → fondos insuficientes
- No se puede escribir en el archivo de log → error de I/O
Para cada error, debes:
- Mostrar un mensaje claro y útil al usuario.
- No permitir que el programa se caiga.
- Registrar el error en un log (si es posible).
Este escenario es súper realista. Y cada tipo de error necesita un tratamiento diferente.
🧠 Cómo leer el enunciado (modo excepción ON)
Aquí no solo estamos buscando "qué estructuras usar", sino qué puede salir mal en cada paso.
| Paso del flujo | ¿Qué puede fallar? | Tipo de excepción probable |
|---|---|---|
| Usuario ingresa número de cuenta | Escribe texto en lugar de número | NumberFormatException (unchecked) |
| Buscar cuenta en base de datos | La cuenta no existe | CuentaNoEncontradaException (custom) |
| Usuario ingresa monto | Escribe texto o número negativo | NumberFormatException / validación |
| Validar saldo suficiente | Saldo < monto solicitado | SaldoInsuficienteException (custom) |
| Registrar transacción en archivo | Archivo no accesible, disco lleno, permisos | IOException (checked) |
Ya con esto empezamos a ver:
- Excepciones del sistema que Java ya tiene (
NumberFormatException,IOException) - Excepciones de negocio que nosotros definimos (
CuentaNoEncontradaException,SaldoInsuficienteException)
Pseudocódigo del algoritmo
INICIO
INTENTAR
leer numeroDeComoTexto desde el usuario
INTENTAR
convertir numeroComoTexto a numeroDeCuenta (puede lanzar NumberFormatException)
CAPTURAR NumberFormatException
mostrar "Error: debe ingresar solo números"
terminar flujo
FIN CAPTURAR
INTENTAR
buscar cuenta en base de datos
SI cuenta no existe
lanzar CuentaNoEncontradaException
FIN SI
CAPTURAR CuentaNoEncontradaException
mostrar "La cuenta no existe en el sistema"
terminar flujo
FIN CAPTURAR
leer montoComoTexto desde el usuario
INTENTAR
convertir montoComoTexto a montoRetiro
CAPTURAR NumberFormatException
mostrar "Error: monto inválido"
terminar flujo
FIN CAPTURAR
SI cuenta.saldo < montoRetiro
lanzar SaldoInsuficienteException
FIN SI
cuenta.saldo = cuenta.saldo - montoRetiro
INTENTAR
escribir transacción en archivo de log
CAPTURAR IOException
mostrar "Advertencia: no se pudo registrar la transacción"
// pero el retiro ya se hizo, así que no revertimos
FIN CAPTURAR
mostrar "Retiro exitoso. Saldo actual: " + cuenta.saldo
CAPTURAR SaldoInsuficienteException
mostrar "Fondos insuficientes"
CAPTURAR cualquier otra excepción
mostrar "Error inesperado del sistema"
registrar en log de errores
FIN INTENTAR
FIN
Test de escritorio (simulando escenarios)
Vamos a simular 3 escenarios diferentes:
Escenario 1: Todo sale bien
| Paso | Entrada del usuario | ¿Qué pasa? | Estado de la cuenta |
|---|---|---|---|
| Ingresar número de cuenta | 12345 | Se convierte a int correctamente | - |
| Buscar cuenta | - | Cuenta existe (saldo: $100,000) | saldo = $100,000 |
| Ingresar monto | 20000 | Se convierte a double correctamente | - |
| Validar saldo | - | $100,000 >= $20,000 | - |
| Restar del saldo | - | saldo = $100,000 - $20,000 | saldo = $80,000 |
| Registrar en log | - | Escritura exitosa | - |
| Resultado | - | "Retiro exitoso. Saldo: $80,000" | saldo final = $80,000 |
Escenario 2: Usuario escribe texto en el monto
| Paso | Entrada del usuario | ¿Qué pasa? |
|---|---|---|
| Ingresar número de cuenta | 12345 | Conversión exitosa |
| Buscar cuenta | - | Cuenta existe |
| Ingresar monto | "veinte mil" | Integer.parseInt() lanza NumberFormatException |
| Capturar excepción | - | catch (NumberFormatException e) |
| Resultado | - | "Error: monto inválido" |
El programa no explota, muestra un mensaje claro y termina de forma controlada.
Escenario 3: Saldo insuficiente
| Paso | Entrada del usuario | ¿Qué pasa? |
|---|---|---|
| Ingresar número de cuenta | 12345 | ✅ |
| Buscar cuenta | - | Cuenta existe (saldo: $10,000) |
| Ingresar monto | 50000 | Se convierte correctamente |
| Validar saldo | - | $10,000 < $50,000 |
| Lanzar excepción | - | throw new SaldoInsuficienteException() |
| Capturar excepción | - | catch (SaldoInsuficienteException e) |
| Resultado | - | "Fondos insuficientes" |
☕ Pasando a Java: implementación completa
Clases de excepciones personalizadas
Primero definimos nuestras excepciones de negocio:
// Excepción cuando la cuenta no existe
class CuentaNoEncontradaException extends Exception {
public CuentaNoEncontradaException(String numeroCuenta) {
super("La cuenta " + numeroCuenta + " no existe en el sistema");
}
}
// Excepción cuando no hay saldo suficiente
class SaldoInsuficienteException extends Exception {
private double saldoActual;
private double montoSolicitado;
public SaldoInsuficienteException(double saldoActual, double montoSolicitado) {
super(String.format("Saldo insuficiente. Disponible: $%.2f, Solicitado: $%.2f",
saldoActual, montoSolicitado));
this.saldoActual = saldoActual;
this.montoSolicitado = montoSolicitado;
}
public double getSaldoActual() {
return saldoActual;
}
public double getMontoSolicitado() {
return montoSolicitado;
}
}
¿Por qué extendemos Exception y no RuntimeException?
Porque queremos que sean checked exceptions: el compilador obliga a quien llame nuestros métodos a manejar estos errores.
Esto es útil para errores esperados de negocio.
Clase Cuenta (simple)
class Cuenta {
private String numero;
private double saldo;
public Cuenta(String numero, double saldo) {
this.numero = numero;
this.saldo = saldo;
}
public String getNumero() {
return numero;
}
public double getSaldo() {
return saldo;
}
public void retirar(double monto) throws SaldoInsuficienteException {
if (saldo < monto) {
throw new SaldoInsuficienteException(saldo, monto);
}
saldo -= monto;
}
@Override
public String toString() {
return String.format("Cuenta %s - Saldo: $%.2f", numero, saldo);
}
}
Nota el throws SaldoInsuficienteException en el método retirar():
Eso le dice al compilador y a quien use este método: "ojo que esto puede lanzar esta excepción, manéjala".
Simulador de base de datos (para el ejemplo)
import java.util.*;
class BaseDeDatos {
private static Map<String, Cuenta> cuentas = new HashMap<>();
static {
// Pre-cargar algunas cuentas de prueba
cuentas.put("12345", new Cuenta("12345", 100000));
cuentas.put("67890", new Cuenta("67890", 10000));
}
public static Cuenta buscarCuenta(String numero) throws CuentaNoEncontradaException {
Cuenta cuenta = cuentas.get(numero);
if (cuenta == null) {
throw new CuentaNoEncontradaException(numero);
}
return cuenta;
}
}
Si la cuenta no existe, lanzamos la excepción personalizada.
Sistema de Log (con manejo de IOException)
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
class SistemaLog {
private static final String ARCHIVO_LOG = "transacciones.log";
public static void registrarTransaccion(String numeroCuenta, double monto) throws IOException {
// try-with-resources: garantiza que el archivo se cierre siempre
try (FileWriter fw = new FileWriter(ARCHIVO_LOG, true);
BufferedWriter bw = new BufferedWriter(fw);
PrintWriter out = new PrintWriter(bw)) {
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
out.println(String.format("[%s] Retiro - Cuenta: %s, Monto: $%.2f",
timestamp, numeroCuenta, monto));
} // El archivo se cierra automáticamente aquí, incluso si hay excepción
}
}
Try-with-resources es oro puro 🏆:
Cualquier recurso que implemente AutoCloseable (como archivos, conexiones de BD, streams) se cierra automáticamente al salir del bloque try, incluso si hay excepciones.
Antes teníamos que hacer:
FileWriter fw = null;
try {
fw = new FileWriter("archivo.txt");
// usar fw
} catch (IOException e) {
// manejar error
} finally {
if (fw != null) {
try {
fw.close(); // ¡otro try-catch!
} catch (IOException e) {
// manejar error del cierre
}
}
}
Con try-with-resources:
try (FileWriter fw = new FileWriter("archivo.txt")) {
// usar fw
} catch (IOException e) {
// manejar error
} // fw se cierra solo ✨
El cajero automático (poniendo todo junto)
import java.util.Scanner;
public class CajeroAutomatico {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
try {
System.out.print("Ingrese número de cuenta: ");
String numeroCuentaTexto = scanner.nextLine();
// 1. Convertir a número (puede lanzar NumberFormatException)
// En este caso lo dejamos como String porque las cuentas pueden tener formato
// Pero validamos que sea numérico
if (!numeroCuentaTexto.matches("\\d+")) {
throw new NumberFormatException("El número de cuenta debe ser numérico");
}
// 2. Buscar cuenta (puede lanzar CuentaNoEncontradaException)
Cuenta cuenta = BaseDeDatos.buscarCuenta(numeroCuentaTexto);
System.out.println("Cuenta encontrada: " + cuenta);
// 3. Solicitar monto
System.out.print("Ingrese monto a retirar: ");
String montoTexto = scanner.nextLine();
double monto;
try {
monto = Double.parseDouble(montoTexto);
if (monto <= 0) {
throw new NumberFormatException("El monto debe ser mayor a cero");
}
} catch (NumberFormatException e) {
System.out.println("Error: monto inválido - " + e.getMessage());
return;
}
// 4. Intentar retiro (puede lanzar SaldoInsuficienteException)
cuenta.retirar(monto);
// 5. Registrar en log (puede lanzar IOException)
try {
SistemaLog.registrarTransaccion(cuenta.getNumero(), monto);
} catch (IOException e) {
System.out.println("Advertencia: no se pudo registrar en el log - " + e.getMessage());
// Pero el retiro ya se hizo, así que continuamos
}
// 6. Confirmar éxito
System.out.println("Retiro exitoso!");
System.out.println("Saldo actual: $" + String.format("%.2f", cuenta.getSaldo()));
} catch (CuentaNoEncontradaException e) {
System.out.println("Error: " + e.getMessage());
} catch (SaldoInsuficienteException e) {
System.out.println("Error: " + e.getMessage());
System.out.println("Intenta retirar hasta $" + String.format("%.2f", e.getSaldoActual()));
} catch (NumberFormatException e) {
System.out.println("Error de formato: " + e.getMessage());
} catch (Exception e) {
System.out.println("Error inesperado del sistema: " + e.getMessage());
e.printStackTrace(); // En producción esto iría a un sistema de logs
} finally {
scanner.close();
System.out.println("\nGracias por usar el cajero automático");
}
}
}
🎓 Lecciones clave sobre excepciones
1. Checked vs Unchecked Exceptions
| Tipo | Extiende de | ¿Obliga a manejar? | Cuándo usar |
|---|---|---|---|
| Checked (verificadas) | Exception | Sí, el compilador obliga | Errores esperados y recuperables (cuenta no existe, archivo no se encuentra) |
| Unchecked (no verificadas) | RuntimeException | No, es opcional | Errores de programación (null pointer, división por cero, índice fuera de rango) |
Regla de oro: Si es un error de negocio que sabes que puede pasar y el usuario puede corregir → checked exception. Si es un bug en el código → unchecked exception (o mejor aún, prevenirlo con validaciones).
2. Try-Catch-Finally
try {
// código que puede lanzar excepciones
} catch (TipoExcepcion1 e) {
// manejo específico para TipoExcepcion1
} catch (TipoExcepcion2 e) {
// manejo específico para TipoExcepcion2
} catch (Exception e) {
// captura cualquier otra excepción (catch genérico al final)
} finally {
// se ejecuta SIEMPRE, haya o no excepción
// útil para cerrar recursos, liberar memoria, etc.
}
Orden importante:
Las excepciones más específicas primero, las más genéricas al final.
Si pones catch (Exception e) primero, las demás nunca se ejecutarán (el compilador te avisará).
3. Try-with-resources (Java 7+)
try (Recurso1 r1 = new Recurso1();
Recurso2 r2 = new Recurso2()) {
// usar r1 y r2
} catch (IOException e) {
// manejar error
}
// r1 y r2 se cierran automáticamente
Funciona con cualquier clase que implemente AutoCloseable:
FileInputStream,FileOutputStream,FileReader,FileWriterBufferedReader,BufferedWriterScanner- Conexiones de base de datos (
Connection,Statement,ResultSet) - Sockets, etc.
Ventajas:
- Código más limpio
- No te olvidas de cerrar recursos
- Maneja excepciones en el cierre automáticamente
4. Cuándo lanzar y cuándo capturar
Lanza (throw) cuando:
- Detectas un error pero no sabes cómo manejarlo en ese nivel.
- Quieres que el código que te llamó decida qué hacer.
public void procesarPago(double monto) throws PagoRechazadoException {
if (monto > limiteCredito) {
throw new PagoRechazadoException("Monto excede límite de crédito");
}
// procesar pago
}
Captura (catch) cuando:
- Sabes cómo recuperarte del error (reintentar, usar valor por defecto, avisar al usuario).
- Estás en el punto correcto para manejar el error (ej: en el controlador de una API).
5. No hagas esto (anti-patrones)
Tragar excepciones (swallow exceptions):
try {
algo();
} catch (Exception e) {
// no hacer nada
}
Si haces esto, el error pasa silenciosamente y luego no sabes por qué las cosas no funcionan.
Catch genérico sin registrar:
try {
algo();
} catch (Exception e) {
System.out.println("Hubo un error"); // ¿qué error? ¿dónde?
}
Al menos logea el stack trace: e.printStackTrace() o usa un logger.
Lanzar Exception genérica:
public void metodo() throws Exception { // muy genérico
// ...
}
Mejor lanzar excepciones específicas para que quien te llame sepa qué puede fallar.
🧪 Mini-ejercicios para practicar
1. Validador de edad
Crea un método
validarEdad(String edadTexto)que:
- Convierta el texto a número (maneja
NumberFormatException).- Lance una excepción personalizada
EdadInvalidaExceptionsi la edad es menor a 0 o mayor a 120.- Retorne la edad como
intsi es válida.
2. Lector de archivo con try-with-resources
Crea un método que lea un archivo línea por línea usando
BufferedReadercon try-with-resources. Si el archivo no existe, capturaFileNotFoundExceptiony muestra un mensaje claro.
3. Validador de correo electrónico
Crea un método
validarEmail(String email)que:
- Verifique que contenga un
@y un.después del@.- Lance
EmailInvalidoException(checked) si no cumple.- Use expresiones regulares (regex) para validación avanzada (opcional).
Cierre
Las excepciones no son el enemigo. Son el mecanismo de Java para decirte: "ey, algo no salió como esperabas, pero no voy a dejarte colgado".
Lo importante es:
- Anticipar qué puede fallar (eso lo sacas del enunciado).
- Decidir si es un error esperado (checked) o un bug (unchecked).
- Manejar el error en el lugar correcto (no muy abajo, no muy arriba).
- Informar al usuario con mensajes claros y útiles.
- Registrar (loggear) para debugging y auditoría.
Si haces eso, tus aplicaciones van a ser robustas, mantenibles y no van a explotar al primer viento que sople 🌪️.
Y al final del día, eso es lo que separa un código de estudiante de uno de producción.
Espero que ahora veas las excepciones como lo que son: herramientas de control de flujo para cuando las cosas no salen como planeaste. Porque en este mundo, casi nunca salen 😂
Próximo artículo: Vamos a meter Streams y Programación Funcional en Java, donde todo se vuelve un pipeline de transformaciones. Va a estar 🔥.
Comentarios
Inicia sesión para dejar un comentario
Sin comentarios aún. Sé el primero en compartir tu opinión.
Escrito por
Wilmar Garcia Valderrama
Líder Técnico de Desarrollo en QUIND y Fundador de WillDevp. Apasionado por la arquitectura limpia, microservicios y buenas prácticas de ingeniería.
Artículos relacionados

Colecciones en Java: Pensando como Algoritmista (sin morir en el intento)
Una guía clara y algorítmica sobre las Colecciones en Java. Aprende a leer enunciados, crear pseudocódigo, hacer test de escritorio y resolver usando List, Set y Map.

Cómo está diseñado Java: Objetos, Clases, Herencia, Polimorfismo e Interfaces (Ejemplo Bancario)
Una explicación clara y cercana de cómo está diseñado Java: clases, objetos, herencia, polimorfismo, interfaces y más. Con ejemplos bancarios fáciles de entender y sin enredos.

Primer vistazo a Java: qué tiene y cómo se inicia
Introducción sencilla a Java: ecosistema, JVM, clases, paquetes, módulos y manejo básico de excepciones — te lo explico así como mi abuelita me enseño.