Excepciones en Java — ejercicios con un conversor, fechas y un banco (y el try que puse en el sitio equivocado)

Tenía que sumar las notas de un grupo de 30 alumnos que venían como texto. Como ya sabía que alguna podía venir mal escrita, metí el bucle dentro de un try y capturé la NumberFormatException. El programa no se caía, imprimía «Nota ignorada» una vez… y la suma salía muy baja. Tardé un buen rato en verlo: el try estaba fuera del bucle. En cuanto la tercera nota falló, el resto del try se saltó, y el resto del try era el bucle entero. Las otras 27 notas ni se miraron.

En estos ejercicios vas a decidir dónde va cada try, qué excepción lanzar en cada caso, cómo traducir un error técnico a uno que se entienda y cómo diseñar tu propia familia de excepciones. Antes de las soluciones, tres fragmentos con trampa (el primero es justo mi try mal colocado). Si necesitas repasar antes de empezar, tienes la teoría y la práctica del tema.

Cómo usar este artículo

  1. Lee el enunciado y la salida esperada.
  2. Inténtalo al menos 15 minutos antes de abrir la pista.
  3. Antes de escribir código, decide para cada error: ¿lo lanzo yo o lo lanza Java? ¿Lo trato aquí o lo dejo subir?
  4. Compara con la solución y lee el porqué de cada decisión.

No usamos Scanner para que las salidas sean exactas: las «entradas del usuario» van en arrays. La lógica es la misma que si las leyeras del teclado.

Nivel 1 — Básico: un conversor seguro

Enunciado. Crea la clase Conversor con dos métodos static:

  • int aEntero(String texto, int porDefecto): convierte el texto (quitando espacios) a entero; si no se puede, devuelve porDefecto.
  • int pedirEntre(String[] entradas, int min, int max): simula pedir un número al usuario. Recorre las entradas en orden; para cada una que no sea un entero o esté fuera de [min, max], imprime el motivo y pasa a la siguiente. Devuelve la primera válida. Si ninguna lo es, lanza una IllegalStateException con el número de intentos.

Pruebas y salida esperada:

System.out.println(aEntero("42", 0) + " " + aEntero(" 7 ", 0) + " " + aEntero("3.5", -1) + " " + aEntero("", -1));

System.out.println("Edad:");
int edad = pedirEntre(new String[]{"veinte", "-3", "150", " 19 "}, 0, 120);

System.out.println("Nota:");
try {
    pedirEntre(new String[]{"diez", "11"}, 0, 10);
} catch (IllegalStateException e) {
    System.out.println("  Error: " + e.getMessage());
}
System.out.println("Edad guardada: " + edad);
42 7 -1 -1
Edad:
  "veinte": no es un número entero
  "-3": fuera de rango [0, 120]
  "150": fuera de rango [0, 120]
  Aceptado 19 en el intento 4
Nota:
  "diez": no es un número entero
  "11": fuera de rango [0, 10]
  Error: ninguna entrada válida en 2 intentos
Edad guardada: 19

En pedirEntre, el try va dentro del for. El rango no lanza ninguna excepción por sí solo: compruébalo con un if y usa continue para pasar al siguiente intento.

Nivel 2 — Intermedio: una Fecha que no se deja crear mal

Enunciado. Crea la clase Fecha (día, mes y año, private final):

  • El constructor lanza IllegalArgumentException si el mes no está entre 1 y 12 o si el día no existe en ese mes, con un mensaje que explique por qué (incluye los días que tiene el mes).
  • static boolean esBisiesto(int anio): divisible entre 4 y no entre 100, o divisible entre 400.
  • static int diasDelMes(int mes, int anio).
  • static Fecha parse(String texto) para textos dd/mm/aaaa. Si no hay tres partes, lanza IllegalArgumentException; si alguna parte no es un número, captura la NumberFormatException y lanza en su lugar una IllegalArgumentException con la original como causa.
  • toString() con el formato 12/10/2026.

Pruebas y salida esperada:

String[] textos = {"12/10/2026", "29/02/2024", "29/02/2026", "31/04/2026", "15/13/2026", "1-1-2026", "aa/10/2026"};
for (String t : textos) {
    try {
        System.out.println("OK  " + Fecha.parse(t));
    } catch (IllegalArgumentException e) {
        System.out.println("ERR " + e.getMessage());
        if (e.getCause() != null) {
            System.out.println("    causa: " + e.getCause().getClass().getSimpleName());
        }
    }
}
OK  12/10/2026
OK  29/02/2024
ERR día no válido: 29 (el mes 2 de 2026 tiene 28 días)
ERR día no válido: 31 (el mes 4 de 2026 tiene 30 días)
ERR mes no válido: 13
ERR formato no válido: "1-1-2026" (usa dd/mm/aaaa)
ERR formato no válido: "aa/10/2026" (solo números)
    causa: NumberFormatException

Las excepciones tienen un constructor IllegalArgumentException(String mensaje, Throwable causa). Pasándole la excepción original como segundo argumento, quien capture la nueva puede consultarla con getCause().

Nivel 3 — Desafío: un banco con su propia familia de excepciones

Enunciado. Diseña la jerarquía de errores de un banco y úsala en transferencias:

  • BancoException extends Exception (checked): la madre de todos los problemas del banco.
  • CuentaNoEncontradaException y SaldoInsuficienteException, ambas hijas de BancoException. La segunda guarda cuánto falta (getFaltan()).
  • Cuenta con IBAN y saldo.
  • Banco con un array de cuentas y contador: abrir(iban, saldo), buscar(iban) (lanza CuentaNoEncontradaException) y transferir(origen, destino, cantidad) throws BancoException.
  • transferir debe ser atómica: si algo falla, no se mueve ni un céntimo. Cantidades no positivas y transferencias a la misma cuenta se rechazan con IllegalArgumentException.
  • En el main, si el problema es de saldo, sugiere la cantidad que sí se podría transferir. Al final, comprueba que el dinero total del banco no ha cambiado.

Pruebas y salida esperada:

Banco banco = new Banco(3);
banco.abrir("ES01", 500);
banco.abrir("ES02", 100);
banco.abrir("ES03", 0);
double antes = banco.total();

String[][] ordenes = {
    {"ES01", "ES02", "200"},
    {"ES02", "ES99", "50"},
    {"ES02", "ES01", "400"},
    {"ES02", "ES02", "10"},
    {"ES01", "ES03", "-5"},
    {"ES02", "ES03", "300"}
};
OK  ES01 -> ES02 (200 €)
ERR ES02 -> ES99 (50 €): no existe la cuenta ES99
ERR ES02 -> ES01 (400 €): saldo insuficiente en ES02: faltan 100,00 €. ¿Transferir 300,00 € en su lugar?
ERR ES02 -> ES02 (10 €): origen y destino son la misma cuenta
ERR ES01 -> ES03 (-5 €): cantidad no válida: -5.0
OK  ES02 -> ES03 (300 €)

Correctas: 2 de 6
  ES01:   300,00 €
  ES02:     0,00 €
  ES03:   300,00 €
Dinero total antes: 600,00 € · después: 600,00 €

Para que la transferencia sea atómica, haz todas las comprobaciones (buscar las dos cuentas, mirar el saldo) antes de tocar ningún saldo. Y en el main, el catch de SaldoInsuficienteException tiene que ir antes que el de BancoException.

¿Qué falla aquí?

Tres fragmentos que compilan. Antes de leer la explicación, intenta predecir qué imprimen.

Fragmento 1 — El try fuera de sitio

String[] notas = {"8", "6", "siete", "9", "5"};
int suma = 0;
try {
    for (String n : notas) {
        suma += Integer.parseInt(n);
    }
} catch (NumberFormatException e) {
    System.out.println("Nota ignorada: " + e.getMessage());
}
System.out.println("Suma: " + suma);

Imprime Nota ignorada: For input string: "siete" y Suma: 14, cuando las notas válidas suman 28. Es mi historia del principio. Al fallar "siete", se salta **el resto del try**, que incluye el resto del bucle: el 9 y el 5 nunca se suman. El mensaje dice «ignorada» en singular, pero en realidad se han ignorado tres. La solución es mover el try dentro del for:

for (String n : notas) {
    try {
        suma += Integer.parseInt(n);
    } catch (NumberFormatException e) {
        System.out.println("Nota ignorada: " + e.getMessage());
    }
}

Pregúntate «si esto falla, ¿qué quiero que no se ejecute?». Eso, y solo eso, va dentro del try.

Fragmento 2 — El finally que cambia el valor (o no)

static int intentos() {
    int x = 1;
    try {
        return x;
    } finally {
        x = 2;
    }
}

System.out.println(intentos());

Imprime 1. El finally se ejecuta, sí, y x pasa a valer 2. Pero el valor que se devuelve ya se había calculado y guardado al llegar al return x, antes de entrar en el finally. Cambiar x después no cambia lo que ya está «en camino».

«arreglarlo» poniendo return 2; dentro del finally. Eso sí cambia el resultado, pero también se traga cualquier excepción que viniera del try: el return del finally gana siempre. Nunca pongas return en un finally; úsalo solo para limpiar.

Fragmento 3 — El catch que no hace nada

static double leerNota(String t) {
    double nota = 0;
    try {
        nota = Double.parseDouble(t);
    } catch (NumberFormatException e) {
    }
    return nota;
}

double media = (leerNota("8") + leerNota("6,5") + leerNota("9")) / 3;
System.out.printf("Media: %.2f%n", media);

Imprime Media: 5,67, cuando debería ser 7,83. "6,5" no se puede convertir (lleva coma), el catch vacío se traga el error y leerNota devuelve el 0 inicial como si fuera una nota. Nada en rojo, nada en la consola: un resultado falso que parece verdadero.

Hay dos soluciones razonables: aceptar la coma (t.replace(',', '.')) si es un formato válido para ti, o no capturar la excepción y dejar que suba hasta alguien que sepa qué hacer. Lo que nunca vale es un catch vacío.

Soluciones

Nivel 1 — Conversor seguro

public class Conversor {

    // Intenta convertir; si no puede, devuelve el valor por defecto
    static int aEntero(String texto, int porDefecto) {
        try {
            return Integer.parseInt(texto.trim());
        } catch (NumberFormatException e) {
            return porDefecto;                       // aquí SÍ es correcto no relanzar: es el contrato del método
        }
    }

    // Simula pedir un número: recorre las entradas hasta encontrar una válida dentro del rango
    static int pedirEntre(String[] entradas, int min, int max) {
        for (int i = 0; i < entradas.length; i++) {
            try {
                int n = Integer.parseInt(entradas[i].trim());
                if (n < min || n > max) {
                    System.out.println("  \"" + entradas[i] + "\": fuera de rango [" + min + ", " + max + "]");
                    continue;                        // siguiente intento
                }
                System.out.println("  Aceptado " + n + " en el intento " + (i + 1));
                return n;
            } catch (NumberFormatException e) {
                System.out.println("  \"" + entradas[i] + "\": no es un número entero");
            }
        }
        throw new IllegalStateException("ninguna entrada válida en " + entradas.length + " intentos");
    }

    public static void main(String[] args) {
        System.out.println(aEntero("42", 0) + " " + aEntero(" 7 ", 0) + " " + aEntero("3.5", -1) + " " + aEntero("", -1));

        System.out.println("Edad:");
        int edad = pedirEntre(new String[]{"veinte", "-3", "150", " 19 "}, 0, 120);

        System.out.println("Nota:");
        try {
            pedirEntre(new String[]{"diez", "11"}, 0, 10);
        } catch (IllegalStateException e) {
            System.out.println("  Error: " + e.getMessage());
        }
        System.out.println("Edad guardada: " + edad);
    }
}

Por qué así:

  • **aEntero captura y devuelve un valor por defecto. ¿No dijimos que capturar y «seguir como si nada» era malo? Aquí es distinto: el método promete** en su nombre y en sus parámetros que, si no puede convertir, devolverá porDefecto. Quien lo llama lo sabe y lo eligió. El problema del fragmento 3 era que nadie sabía que el 0 significaba «error».
  • **"3.5" devuelve -1:** Integer.parseInt no acepta decimales. Si quisieras redondear, tendrías que usar Double.parseDouble y convertir.
  • **try dentro del for:** cada entrada tiene su oportunidad, la lección del fragmento 1.
  • **El rango se comprueba con if, no con excepciones:** 19 y 150 son enteros perfectamente válidos para Java; que estén fuera de rango es tu regla. continue salta al siguiente intento sin ejecutar el return.
  • **Si se agotan los intentos, IllegalStateException:** el método no puede cumplir lo que promete (devolver un número válido), así que no puede devolver nada razonable. Lanzar es la única opción honesta, y es unchecked porque la situación indica que quien lo llama le pasó datos insuficientes.

Nivel 2 — Fecha validada

public class Fecha {

    private final int dia;
    private final int mes;
    private final int anio;

    public Fecha(int dia, int mes, int anio) {
        if (mes < 1 || mes > 12) {
            throw new IllegalArgumentException("mes no válido: " + mes);
        }
        int max = diasDelMes(mes, anio);
        if (dia < 1 || dia > max) {
            throw new IllegalArgumentException("día no válido: " + dia + " (el mes " + mes + " de " + anio + " tiene " + max + " días)");
        }
        this.dia = dia;                              // solo se guarda si todo es correcto
        this.mes = mes;
        this.anio = anio;
    }

    public static boolean esBisiesto(int anio) {
        return (anio % 4 == 0 && anio % 100 != 0) || anio % 400 == 0;
    }

    public static int diasDelMes(int mes, int anio) {
        switch (mes) {
            case 2:
                return esBisiesto(anio) ? 29 : 28;
            case 4: case 6: case 9: case 11:
                return 30;
            default:
                return 31;
        }
    }

    // "12/10/2026" -> Fecha. Cualquier problema se convierte en IllegalArgumentException
    public static Fecha parse(String texto) {
        String[] partes = texto.trim().split("/");
        if (partes.length != 3) {
            throw new IllegalArgumentException("formato no válido: \"" + texto + "\" (usa dd/mm/aaaa)");
        }
        try {
            int d = Integer.parseInt(partes[0]);
            int m = Integer.parseInt(partes[1]);
            int a = Integer.parseInt(partes[2]);
            return new Fecha(d, m, a);
        } catch (NumberFormatException e) {
            // traducimos el error técnico a uno del dominio, y guardamos el original como causa
            throw new IllegalArgumentException("formato no válido: \"" + texto + "\" (solo números)", e);
        }
    }

    @Override
    public String toString() {
        return String.format("%02d/%02d/%04d", dia, mes, anio);
    }
}

Por qué así:

  • Validar en el constructor garantiza que no puede existir una Fecha inválida: si el constructor lanza, el objeto no llega a crearse. Es la versión «con excepciones» del setNota de clases y objetos.
  • El mes se comprueba antes que el día porque para saber cuántos días tiene el mes, el mes tiene que ser válido. El orden de las comprobaciones sigue importando.
  • **parse traduce NumberFormatException a IllegalArgumentException.** A quien usa Fecha le da igual que por dentro se use parseInt; le importa que «la fecha no es válida». Así, un solo catch (IllegalArgumentException e) sirve para todos los casos.
  • La excepción original se guarda como causa (segundo argumento). No se pierde información: si alguien necesita el detalle técnico, ahí está con getCause().
  • **¿Y si new Fecha(...) lanza dentro del try de parse?** Lanza una IllegalArgumentException, que no es una NumberFormatException (es su madre, no su hija). El catch no la captura, sube tal cual y llega al main con su mensaje original («día no válido…»). Es justo lo que queremos.

2024 es bisiesto (divisible entre 4 y no entre 100) y 2026 no. Por eso el 29/02/2024 existe y el 29/02/2026 no. El año 2000 también fue bisiesto por la regla del 400; el 1900, no.

Nivel 3 — Banco con excepciones propias

// Madre de todos los problemas del banco: capturándola, se capturan todas sus hijas
public class BancoException extends Exception {

    public BancoException(String mensaje) {
        super(mensaje);
    }
}
public class CuentaNoEncontradaException extends BancoException {

    private final String iban;

    public CuentaNoEncontradaException(String iban) {
        super("no existe la cuenta " + iban);
        this.iban = iban;
    }

    public String getIban() { return iban; }
}
public class SaldoInsuficienteException extends BancoException {

    private final double faltan;

    public SaldoInsuficienteException(String iban, double faltan) {
        super(String.format("saldo insuficiente en %s: faltan %.2f €", iban, faltan));
        this.faltan = faltan;
    }

    public double getFaltan() { return faltan; }
}
public class Cuenta {

    private final String iban;
    private double saldo;

    public Cuenta(String iban, double saldo) {
        this.iban = iban;
        this.saldo = saldo;
    }

    public String getIban()  { return iban; }
    public double getSaldo() { return saldo; }

    // Solo el banco las usa, y solo después de validarlo todo
    void sacar(double cantidad)  { saldo -= cantidad; }
    void meter(double cantidad)  { saldo += cantidad; }
}
public class Banco {

    private final Cuenta[] cuentas;
    private int numCuentas = 0;

    public Banco(int capacidad) {
        cuentas = new Cuenta[capacidad];
    }

    public void abrir(String iban, double saldoInicial) {
        if (numCuentas == cuentas.length) {
            throw new IllegalStateException("el banco está lleno");
        }
        cuentas[numCuentas++] = new Cuenta(iban, saldoInicial);
    }

    public Cuenta buscar(String iban) throws CuentaNoEncontradaException {
        for (int i = 0; i < numCuentas; i++) {
            if (cuentas[i].getIban().equals(iban)) {
                return cuentas[i];
            }
        }
        throw new CuentaNoEncontradaException(iban);
    }

    public void transferir(String origen, String destino, double cantidad) throws BancoException {
        if (cantidad <= 0) {
            throw new IllegalArgumentException("cantidad no válida: " + cantidad);
        }
        Cuenta o = buscar(origen);                   // buscamos las DOS antes de mover un céntimo
        Cuenta d = buscar(destino);
        if (o == d) {
            throw new IllegalArgumentException("origen y destino son la misma cuenta");
        }
        if (cantidad > o.getSaldo()) {
            throw new SaldoInsuficienteException(origen, cantidad - o.getSaldo());
        }
        o.sacar(cantidad);                           // a partir de aquí ya no puede fallar nada
        d.meter(cantidad);
    }

    public double total() {
        double t = 0;
        for (int i = 0; i < numCuentas; i++) {
            t += cuentas[i].getSaldo();
        }
        return t;
    }

    public void listar() {
        for (int i = 0; i < numCuentas; i++) {
            System.out.printf("  %s: %8.2f €%n", cuentas[i].getIban(), cuentas[i].getSaldo());
        }
    }
}
public class Main {
    public static void main(String[] args) {
        Banco banco = new Banco(3);
        banco.abrir("ES01", 500);
        banco.abrir("ES02", 100);
        banco.abrir("ES03", 0);
        double antes = banco.total();

        String[][] ordenes = {
            {"ES01", "ES02", "200"},
            {"ES02", "ES99", "50"},
            {"ES02", "ES01", "400"},
            {"ES02", "ES02", "10"},
            {"ES01", "ES03", "-5"},
            {"ES02", "ES03", "300"}
        };

        int correctas = 0;
        for (String[] o : ordenes) {
            String texto = o[0] + " -> " + o[1] + " (" + o[2] + " €)";
            try {
                banco.transferir(o[0], o[1], Double.parseDouble(o[2]));
                correctas++;
                System.out.println("OK  " + texto);
            } catch (SaldoInsuficienteException e) {
                System.out.printf("ERR %s: %s. ¿Transferir %.2f € en su lugar?%n",
                        texto, e.getMessage(), Double.parseDouble(o[2]) - e.getFaltan());
            } catch (BancoException e) {
                System.out.println("ERR " + texto + ": " + e.getMessage());
            } catch (IllegalArgumentException e) {
                System.out.println("ERR " + texto + ": " + e.getMessage());
            }
        }

        System.out.println("\nCorrectas: " + correctas + " de " + ordenes.length);
        banco.listar();
        System.out.printf("Dinero total antes: %.2f € · después: %.2f €%n", antes, banco.total());
    }
}

Por qué así:

  • **Una madre común (BancoException)** permite dos niveles de detalle. transferir declara solo throws BancoException, aunque lance dos tipos distintos; y en el main tratamos a una hija de forma especial (el saldo, con su sugerencia) y al resto de la familia de golpe. Si mañana añades CuentaBloqueadaException extends BancoException, transferir no cambia su firma y el main ya la captura.
  • **El catch de la hija va antes que el de la madre.** Si BancoException fuera primero, capturaría también el saldo insuficiente y nunca se mostraría la sugerencia (y Java no compilaría el catch de abajo).
  • Atomicidad: todas las comprobaciones (cantidad, las dos cuentas, que no sean la misma, el saldo) van antes de sacar y meter. Si se buscara el destino después de sacar el dinero del origen, la orden a ES99 haría desaparecer 50 €. La última línea de la salida lo demuestra: entraron 600 € y quedan 600 €.
  • **sacar y meter sin modificador** (visibilidad de paquete): las usa el Banco, pero no están pensadas para el resto del programa. Así nadie puede saltarse las validaciones de transferir.
  • **getFaltan() se usa para calcular la sugerencia:** pedías 400, faltan 100, se pueden transferir 300. Es el dato útil que la excepción lleva dentro, como en el cajero de la práctica.

Declarar en transferir throws CuentaNoEncontradaException, SaldoInsuficienteException, ... con todas las hijas, una a una. Funciona, pero cada excepción nueva obliga a cambiar la firma y todos los sitios que la llaman. Declarar la madre es más limpio.

Míralo paso a paso en Python Tutor

Python Tutor ejecuta el código línea a línea y dibuja las variables y las llamadas. Vamos a ver en directo el fragmento 2, el del finally que «cambia» el valor. Elige Java, pega esto y avanza con Next:

public class Tutor {
    static int intentos() {
        int x = 1;
        try {
            return x;
        } finally {
            x = 2;
        }
    }
    public static void main(String[] args) {
        System.out.println(intentos());
    }
}

Salida:

1

Qué observar:

  1. Al entrar en intentos, aparece un marco con x = 1.
  2. Al ejecutar return x;, fíjate en que la ejecución no sale del método: salta al finally. En ese momento el 1 ya está decidido como valor de retorno, aunque el método todavía no haya terminado.
  3. Dentro del finally, x cambia a 2 en el marco… pero el valor que se va a devolver sigue siendo 1.
  4. Al salir, main recibe ese 1. La variable cambió; lo que se devolvía, no. Ahora prueba a cambiar x = 2; por return 2; y mira cómo el finally «pisa» el resultado.

Chuletario: excepciones en Java

Lo esencial en 30 segundos

  • Cuando salta una excepción en un try, **el resto del try se salta**; Java entra en el primer catch que encaje y después sigue. finally se ejecuta siempre.
  • catch de lo más concreto a lo más general. Hija antes que madre.
  • Checked (Exception, IOException): captúrala o declárala con throws. Unchecked (RuntimeException): no obliga.
  • throw new X("mensaje") lanza; throws X avisa en la firma. Excepción propia: class MiException extends Exception + super(mensaje).
  • Nunca un catch vacío: informa, corrige o vuelve a lanzar (si traduces, guarda la causa).

Sintaxis

Quiero…CódigoResultado
Intentar algo que puede fallartry { … } catch (NumberFormatException e) { … }el programa sigue
Varios tipos, tratamientos distintosvarios catch, del concreto al generalentra en el primero que encaje
Varios tipos, mismo tratamientocatch (NumberFormatException | ArrayIndexOutOfBoundsException e)multi-catch
Algo que se ejecute siemprefinally { … }también con return o break
Ver qué ha pasadoe.getMessage()el texto del error
Ver el tipoe.getClass().getSimpleName()"NumberFormatException"
Lanzar un errorthrow new IllegalArgumentException("…")corta el método
Avisar en la firmavoid f() throws IOExceptionquien llame debe tratarla
Excepción propia checkedclass X extends Exception { X(String m) { super(m); } }obliga a tratarla
Excepción propia uncheckedclass X extends RuntimeExceptionno obliga
Traducir guardando la causathrow new IllegalArgumentException("…", e)getCause() devuelve e
Cerrar un fichero solotry (BufferedReader r = new BufferedReader(new FileReader(f))) { … }se cierra al salir

Plantillas de los ejercicios

Reintentar con el try dentro del bucle (Conversor):

for (String t : entradas) {
    try {
        int n = Integer.parseInt(t.trim());
        if (n < min || n > max) continue;
        return n;
    } catch (NumberFormatException e) {
        System.out.println("no es un número: " + t);
    }
}
throw new IllegalStateException("ninguna entrada válida");

Edad → aceptado 19 en el intento 4

Traducir una excepción técnica (Fecha):

try {
    return new Fecha(Integer.parseInt(p[0]), Integer.parseInt(p[1]), Integer.parseInt(p[2]));
} catch (NumberFormatException e) {
    throw new IllegalArgumentException("formato no válido: " + texto, e);
}

"aa/10/2026" → formato no válido…, causa NumberFormatException

Familia de excepciones (Banco):

try {
    banco.transferir(origen, destino, cantidad);
} catch (SaldoInsuficienteException e) {      // la hija, primero
    // usar e.getFaltan()
} catch (BancoException e) {                  // el resto de la familia
    System.out.println(e.getMessage());
}

600 € antes → 600 € después

Errores que más se repiten

SíntomaCausa probable
El programa se para con una traza rojaExcepción sin capturar: busca la primera línea de tu código en la traza
unreported exception X; must be caught or declared to be thrownExcepción checked sin try/catch ni throws
exception X has already been caughtUn catch general antes que uno concreto
exception X is never thrown in body of corresponding try statementCapturas una checked que el try no puede lanzar
Se procesa solo una parte de los datosEl try está fuera del bucle
Resultado raro sin ningún error en pantallaUn catch vacío o que devuelve un valor «por si acaso»
For input string: "7,5"Decimal con coma: parseDouble solo acepta punto
empty StringHas intentado convertir un texto vacío
Una operación fallida deja datos a mediasModificas el estado antes de terminar de validar

Antes de entregar, pregúntate

  1. ¿Cada try envuelve solo lo que no debe ejecutarse si algo falla (y está dentro del bucle cuando toca)?
  2. ¿Mis catch van de lo más concreto a lo más general, y ninguno está vacío?
  3. ¿He elegido bien entre checked y unchecked, y mis excepciones propias llevan un mensaje claro y los datos útiles?
  4. ¿Valido todo antes de modificar nada, para que un error no deje los datos a medias?
Sigue aprendiendoTodos los artículos de Java

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *