← CC2008

Excepciones

Semestre 02, 2026

Contenido de la sesión

Excepciones: qué son y por qué detienen el programa.
Captura de errores con try, catch y finally.
Lanzar y propagar con throw y throws.
Excepciones personalizadas y buenas prácticas.

Excepciones

Definición

Una excepción es una situación anormal que ocurre durante la ejecución del programa e interrumpe su flujo normal.

El usuario escribe "hola" cuando el programa esperaba un número.
Intentamos nombres.get(100) cuando solamente existen tres elementos.
Sin control

El programa lanza un error y puede detenerse abruptamente.

Los tres caminos

Al enfrentarnos a una excepción solo existen tres salidas posibles.

Manejarla
  • La atrapamos con try, catch y finally, y nos hacemos cargo aquí mismo.
  • El programa sigue corriendo.
Propagarla
  • No es mi clavo: la declaramos con throws y se la entregamos a quien nos llamó.
  • El problema sigue vivo, solo que ahora es de alguien más.
Morir
  • Nadie la captura en toda la cadena y la excepción llega hasta el final.
  • Java detiene el programa y muestra el error en la consola.

Flujo de una excepción

1Java crea un objeto Exception
  • Contiene el tipo del error, un mensaje y el rastro de llamadas.
2Ese objeto se lanza
  • La ejecución del método se interrumpe en esa línea.
3Se busca un catch que lo maneje
  • Si existe, el control pasa a ese bloque.
4Si no existe, se propaga
  • Sube al método que llamó, y así hasta detener el programa.

Jerarquía en Java


Throwable
 ├─ Error              (fallos graves: no capturar)
 │   ├─ OutOfMemoryError
 │   └─ StackOverflowError
 └─ Exception
     ├─ RuntimeException   (no chequeadas)
     │   ├─ NullPointerException
     │   └─ ArithmeticException
     ├─ IOException        (chequeadas)
     └─ SQLException

Tipos de excepciones

Chequeadas

Verificadas por el compilador. Obligan a usar try/catch o declarar throws. Representan condiciones recuperables, como leer un archivo que podría no existir.

No chequeadas

Heredan de RuntimeException. Suelen indicar errores de programación o mal uso de una API. El compilador no obliga a capturarlas.

Ejemplos chequeadas: IOException, SQLException.
Ejemplos no chequeadas: NullPointerException, IllegalArgumentException.

Excepciones comunes

ExcepciónCuándo ocurre
NullPointerExceptionUsamos un objeto que vale null
ArithmeticExceptionDivisión entera entre cero
NumberFormatExceptionConvertir a número un texto que no lo es
IndexOutOfBoundsExceptionPedir una posición que no existe
InputMismatchExceptionScanner recibe un tipo distinto al esperado
IOExceptionFalla una operación de entrada o salida

Excepciones al usar ArrayList


ArrayList<String> nombres = new ArrayList<>();
nombres.add("Ana");

// IndexOutOfBoundsException: solo existe el índice 0
System.out.println(nombres.get(5));
Pedir un índice mayor o igual a size() lanza IndexOutOfBoundsException.
Eliminar elementos dentro de un for-each lanza ConcurrentModificationException.
Prevención

Validar con size() o isEmpty() antes de acceder evita la mayoría de estos errores sin necesidad de capturarlos.

try y catch

try: código que podría producir una excepción.
catch: qué hacer si la excepción ocurre.

try {
    int numero = Integer.parseInt("hola");
} catch (NumberFormatException e) {
    System.out.println("Debe ingresar un número.");
}
Resultado

Debe ingresar un número.

Ejemplo resuelto con Scanner


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.");
}
Scanner teclado = new Scanner(System.in);

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.");
}
try {
    String entrada = teclado.nextLine();

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.");
}
int edad = Integer.parseInt(entrada);

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.");
}
catch (NumberFormatException e)

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.

Varios bloques catch


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");
}
Orden

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.

Multi-catch

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();
}

Información de la excepción


try {
    int resultado = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println(e.getMessage());
    e.printStackTrace();
}
getMessage() devuelve la descripción del error: / by zero
printStackTrace() muestra el rastro de llamadas hasta la línea que falló.

El bloque finally

try: intentar la operación.
catch: manejar el error.
finally: ejecutar al terminar, ocurra o no una excepción.

try {
    System.out.println("Procesando...");
} catch (Exception e) {
    System.out.println("Ocurrió un error.");
} finally {
    System.out.println("Proceso finalizado.");
}

Cierre manual con finally

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();
    }
}
Costo

Declarar la variable afuera, comprobar que no sea null y cerrarla a mano: tres pasos que es fácil olvidar.

Cierre manual y cierre automático

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");
}
Diferencia

La versión automática elimina la variable externa, la comprobación de null y el bloque finally completo.

Los paréntesis del try

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");
}
try (FileReader fr = ...)

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");
}
System.out.println(fr.read());

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");
}
catch (IOException e)

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.

Cuándo usar cada forma

try de recursos

Cuando trabajamos con archivos, conexiones o cualquier objeto que se cierre. Es la forma recomendada.

finally

Cuando la tarea final no es cerrar un recurso: mostrar un mensaje, restablecer una bandera o liberar algo que Java no sabe cerrar.

Requisito

Solo pueden ir entre los paréntesis los objetos que Java sabe cerrar, es decir los que implementan la interfaz AutoCloseable.

Lanzar una excepción

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;
}
Efecto

Si b vale cero, el método se interrumpe en esa línea y la excepción viaja hacia quien lo invocó.

Propagar una excepción

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"));
}
Advertencia

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.

Es obligatorio para las excepciones chequeadas.
El método no resuelve el problema: lo anuncia y lo entrega hacia arriba.

Opciones de quien llama al método

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();

}
Volver a declararla

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.

Cadena de propagación

Mientras nadie la capture, la excepción sigue subiendo por los métodos que llamaron.

1Files.readString lanza la excepción
  • El archivo config.json no existe y falla la lectura.
2cargar declara throws IOException
  • No la captura: anuncia el peligro y la deja subir.
3iniciar vuelve a declarar throws
  • Repite el mismo letrero y sigue pasando el problema.
4main decide
  • Si tampoco la captura, el programa se detiene y muestra el error.

Propagación entre clases

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");
    }
}

Captura al final de la cadena


// 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 recorrido

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.

Excepciones personalizadas

Extiende Exception para una chequeada, o RuntimeException para una no chequeada.


public class SaldoInsuficienteException extends RuntimeException {
    public SaldoInsuficienteException(String mensaje) {
        super(mensaje);
    }
}

Excepciones con contexto adicional


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; }
}

Uso de una excepción propia


public void retirar(double monto) {
    if (monto > saldo) {
        throw new SaldoInsuficienteException("Saldo insuficiente");
    }

    saldo -= monto;
}
Ventaja

El nombre de la excepción comunica el problema del dominio mucho mejor que un mensaje genérico de error.

Código semántico

Definición

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");
}

Buenas prácticas

  • Captura solo lo que puedes manejar.
  • No silencies excepciones: nada de catch vacío.
  • Valida precondiciones con IllegalArgumentException.
  • Prefiere validar antes que capturar cuando el error es evitable.
  • Usa nombres claros: SaldoInsuficienteException.
  • Captura el tipo específico en lugar de Exception cuando sea posible.

Resumen

Una excepción interrumpe el flujo normal del programa.
try intenta la operación y catch controla el error.
finally se ejecuta siempre al finalizar el bloque.
Las chequeadas obligan a manejarlas; las no chequeadas, no.
throw lanza y throws propaga hacia quien invoca.
Las excepciones propias describen errores del dominio.