CodeGym /Cursos /Kotlin SELF /Introduccion a las excepciones

Introduccion a las excepciones

Kotlin SELF
Nivel 12, Lección 1
Disponible

1. Errores de compilacion y errores de ejecucion

Si hasta ahora percibias los errores como «subrayado rojo en el IDE», ahora es importante separar dos mundos. Hay errores que Kotlin encuentra antes de ejecutar (en la etapa de compilacion), y hay problemas que se manifiestan solo durante la ejecucion del programa. Las excepciones pertenecen al segundo tipo: aparecen cuando el programa ya esta en marcha y se encuentra con una situacion con la que no puede continuar «segun el plan».

Para verlo mas claro, es util comparar estas cosas en una tabla:

Que es Cuando ocurre Que ves Ejemplo
Error de compilacion Antes de ejecutar el programa El IDE se queja, kotlinc no compila
val x: Int = "hola"
Excepcion (runtime exception) Durante la ejecucion El programa se cae, se imprime un mensaje y el stack trace
println(10 / 0)

Una excepcion no es «otro if». Es un mecanismo de emergencia: como una alarma contra incendios. Si se dispara y nadie sabe que hacer, la gente (es decir, la ejecucion del programa) corre hacia la salida (es decir, el programa termina).

En Kotlin las excepciones estan representadas por objetos que son descendientes de Throwable. A grandes rasgos: Throwable → Exception / Error. Normalmente en el codigo de aplicacion nos encontramos con Exception, y Error es mas bien una historia de problemas graves del entorno de ejecucion (por ejemplo, falta de memoria).

Que es una excepcion

Imagina que tu programa es un tren que circula por las vias de las lineas de codigo. El flujo normal de ejecucion es: el tren sigue la ruta: linea 1, linea 2, linea 3… Una excepcion es una situacion en la que de repente hay un bloque de hormigon en las vias: no se puede seguir, y el tren no va a «pasar con cuidado» por encima del obstaculo. Se detiene, y el control se transfiere a un mecanismo especial de gestion de errores.

Formalmente (y es muy importante recordarlo): cuando se lanza una excepcion, la ejecucion normal se interrumpe. Esto significa literalmente: «la siguiente linea despues de la problematica no se ejecutara», si la excepcion no se captura.

Mini-ejemplo: el clasico «division por cero»:

fun main() {
    println("start")
    val a = 10
    val b = 0
    println(a / b)        // ArithmeticException
    println("end")        // no se ejecutara
}

Fijate: la linea println("end") parece inocente, pero no se ejecutara. Porque el programa «se salio» de la ruta normal en la linea a / b.

2. La excepcion interrumpe la ejecucion y no deja «seguir adelante»

Con las excepciones, a los principiantes casi siempre les desconcierta una cosa: estamos acostumbrados a que el programa sea una «lista de comandos» y los ejecute en orden. Y de repente resulta que el orden puede interrumpirse de golpe. Por eso repito la idea una vez mas, pero con otras palabras: una excepcion no es un «mensaje de error», es un cambio de control.

Cuando aparece una excepcion, la funcion actual termina no con return, sino de forma abrupta. Y despues Kotlin/JVM intenta encontrar un lugar preparado para manejar esa excepcion. Si no existe tal lugar, el programa termina y ves el mensaje de error y la pila de llamadas.

Otro detalle importante: una excepcion puede ocurrir no donde escribiste el codigo «peligroso», sino «profundamente» dentro de las funciones que llamaste. Por ejemplo, llamas a toInt(), y dentro Kotlin intenta parsear la cadena y, si no puede, lanza NumberFormatException. Esa excepcion «sale disparada» hacia afuera hasta tu codigo.

Ejemplo:

fun main() {
    val text = "12a"
    val n = text.toInt()  // NumberFormatException
    println("n = $n")     // no se ejecutara
}

Es decir, tu no la creaste, pero la excepcion aparece igual porque la funcion de biblioteca decidio: «no se puede continuar».

3. De donde salen las excepciones

En esta etapa es mas util no intentar memorizar cien nombres de excepciones, sino ver los «tipos de situaciones» que las provocan. En programas sencillos de aprendizaje, lo mas comun es que aparezcan tres familias de problemas: aritmetica, conversion de cadenas e indexacion de arrays.

Aritmetica: division por cero

Con la aritmetica todo es intuitivo: algunas operaciones no estan definidas matematicamente. La JVM no va a «adivinar» lo que querias decir y lanzara una excepcion.

fun main() {
    val total = 100
    val days = 0
    val perDay = total / days
    println(perDay) // no llegaremos aqui
}

Conversion de cadena a numero: «esto no es un numero»

Ya conoces la via segura toIntOrNull(). Pero toInt() es la variante «estricta». Si la cadena no es un entero correcto, aparece NumberFormatException.

fun main() {
    val raw = "cuarenta y dos"
    val n = raw.toInt()      // NumberFormatException
    println(n)
}

Indices del array: te saliste de los limites

Los arrays tienen tamano fijo, y los indices deben estar en el rango 0..lastIndex. Si no, obtendras una excepcion en tiempo de ejecucion (en la JVM suele ser ArrayIndexOutOfBoundsException).

fun main() {
    val xs = intArrayOf(10, 20, 30)
    println(xs[2])  // 30
    println(xs[5])  // ArrayIndexOutOfBoundsException
}

La trampa psicologica aqui es: «pero el indice es un numero, ¡lo he parseado!». Si, es un numero. Pero no cualquier numero: uno valido. Y la excepcion justamente dice: «se ha violado la regla».

4. Jerarquia de excepciones: Throwable, Exception, Error

Cuando ves un mensaje de error, casi siempre aparece el nombre del tipo de excepcion. Para no percibirlo como un conjunto de hechizos aleatorios, conviene entender el nivel superior de la jerarquia.

La raiz de la jerarquia es Throwable. Tiene dos «descendientes de primer nivel» importantes: Exception y Error. La idea es esta: Exception es algo que, en principio, puede formar parte de la vida normal del programa (error de entrada, recurso no disponible, etc.), y Error suelen ser problemas graves del entorno que no tiene sentido «arreglar» a nivel de tu logica (por ejemplo, OutOfMemoryError).

En esta etapa nos basta con este esquema:

flowchart TD
    T[Throwable]
    E[Exception]
    ER[Error]
    T --> E
    T --> ER
    E --> NFE[NumberFormatException]
    E --> AE[ArithmeticException]
    E --> AIOOBE[ArrayIndexOutOfBoundsException]

Importante: esto no significa que estes obligado a saber toda la jerarquia de memoria. Pero cuando ves ArithmeticException, deberias pensar: «vale, esto es una Exception, asi que es un error del nivel de ejecucion de mi escenario».

Causa y lugar: nombre de la excepcion y stack trace

Cuando el programa se cae, el principiante a menudo siente: «se rompio en algun sitio». Pero el mecanismo de excepciones esta hecho precisamente para darte dos pistas: que paso (la causa) y donde paso (el lugar).

La causa suele estar codificada en el nombre del tipo y en el mensaje. Por ejemplo, NumberFormatException significa literalmente: «la cadena no encaja con el formato de un numero». El lugar normalmente se ve en el stack trace: es la cadena de llamadas a funciones que llevo al error. Kotlin/JVM, cuando una excepcion no se maneja, imprime el stack trace automaticamente.

Mini-ejemplo que crea el error a proposito:

fun main() {
    throw ArithmeticException("Ejemplo de excepcion")
}

Salida aproximada (puede variar la forma, pero la idea es la misma):

Exception in thread "main" java.lang.ArithmeticException: Ejemplo de excepcion
    at MainKt.main(Main.kt:2)

Por ahora no necesitas saber leer el stack trace perfectamente (aprenderemos a hacerlo por separado). Pero ya es util saberlo: si el programa se cayo, normalmente dejo un «recibo» de donde exactamente ocurrio.

5. Ejemplo practico

Para que las excepciones no se queden en abstracciones, sigamos escribiendo una pequena aplicacion de consola. Que se llame BudgetBuddy: el usuario introduce gastos de varios dias, y el programa calcula la suma y el «promedio por dia».

De momento escribimos de la forma mas simple posible, sin manejo de excepciones (eso sera la siguiente leccion). Nuestro objetivo aqui es ver como se rompe exactamente el flujo de ejecucion.

Version ingenua: «todo segun el plan»

Empecemos con un escenario simple: leemos el numero de dias, luego leemos los gastos por dias en un array y despues calculamos la suma.

fun main() {
    print("¿Cuantos dias? ")
    val days = readln().toInt()

    val expenses = IntArray(days)

    for (i in 0 until days) {
        print("Gasto del dia ${i + 1}: ")
        expenses[i] = readln().toInt()
    }

    var total = 0
    for (x in expenses) total += x

    println("Total: $total")
}

Si el usuario introduce numeros normales, todo va bien. Pero los «numeros normales» son un sueno irreal.

Escenario de caida: el usuario introdujo algo que no es un numero

Supongamos que el usuario introduce lo siguiente:

  • «¿Cuantos dias?» → 3
  • «Gasto del dia 1» → 100
  • «Gasto del dia 2» → cien (o 100€)

En la linea readln().toInt() ocurrira NumberFormatException, y el programa terminara. Ademas, esto ocurrira dentro del bucle, lo que significa que ni siquiera llegaremos a la suma ni al println("Total...") final.

Escenario de caida: el usuario introdujo 0 dias (y nosotros queriamos el promedio)

Anadamos el promedio. De forma ingenua:

fun main() {
    print("¿Cuantos dias? ")
    val days = readln().toInt()

    print("Suma de gastos: ")
    val total = readln().toInt()

    val avg = total / days
    println("Promedio por dia: $avg")
}

Si days = 0, en la linea total / days habra ArithmeticException. Y de nuevo: la linea println(...) no se ejecutara.

Lo especialmente enganoso aqui es: aunque days se haya parseado con exito (es decir, realmente sea un numero), aun puede ser invalido para tu logica. Las excepciones a menudo no ocurren en el parseo, sino en el uso del valor.

Escenario de caida: uso incorrecto del indice

Imaginemos que queremos mostrar el «ultimo gasto introducido», pero nos equivocamos y tomamos el indice days, y no days - 1. (Es un dolor que han vivido generaciones de programadores, y la tradicion continuara.)

fun main() {
    val expenses = intArrayOf(100, 200, 300)
    val days = expenses.size

    println(expenses[days]) // days == 3, e indices 0..2
}

Esto dara ArrayIndexOutOfBoundsException. El programa volvera a terminar de forma abrupta.

Que ocurre con una excepcion: mini-esquema

Para consolidar el modelo, es util imaginar el comportamiento del programa como un diagrama. Nuestro programa ejecuta lineas mientras todo va bien. Si una linea lanza una excepcion, se produce una «salida de emergencia» de la funcion actual y se sube por las llamadas.

flowchart TD
    A[Ejecutamos linea a linea] --> B{¿Ocurrio un error?}
    B -->|no| A
    B -->|si: excepcion| C[Interrumpimos la funcion actual]
    C --> D[Buscamos un manejador mas arriba en la pila]
    D --> E{¿Encontramos un manejador?}
    E -->|no| F[Imprimimos el stack trace y terminamos el programa]
    E -->|si| G[Transferimos el control al manejador]

Hoy te basta con recordar la rama «no encontramos manejador → el programa termino». De la rama «encontramos manejador» hablaremos en detalle en la siguiente leccion, cuando aparezca try/catch.

6. Errores tipicos

Error n.º 1: confundir un error de compilacion con una excepcion en tiempo de ejecucion.
Si el IDE subraya el codigo antes de ejecutar, es compilacion: el programa ni siquiera arranca. Si el programa se ha iniciado, ha alcanzado a imprimir algo y de repente «se ha caido» y ha mostrado un texto largo, es una excepcion, es decir, un problema de tiempo de ejecucion.

Error n.º 2: esperar que el codigo despues de la «linea problematica» se ejecute de todas formas.
Esto es lo mas frecuente. Parece que println("end") tiene que imprimirse, porque esta «despues» de la division. Pero la excepcion rompe la linealidad: la ejecucion se interrumpe. En la cabeza hay que cambiar el interruptor: «excepcion = salida de emergencia».

Error n.º 3: creer que si usamos toIntOrNull(), ya no habra excepciones.
toIntOrNull() realmente te salva de NumberFormatException, pero no protege de la division por cero, de salirte de los limites del array ni de errores logicos. No es «armadura contra todo», sino una herramienta contra una causa concreta.

Error n.º 4: ignorar el nombre de la excepcion y leer el stack trace como una «sabana aterradora».
Incluso sin habilidades de depuracion, el nombre de la excepcion suele ser muy informativo: NumberFormatException — no es un numero, ArithmeticException — aritmetica, ArrayIndexOutOfBoundsException — indice fuera de rango. Kotlin/JVM te da estas pistas a proposito.

Error n.º 5: pensar que una excepcion es «el fin del mundo» y por eso es mejor «no tocarla».
En realidad las excepciones son un mecanismo normal de trabajo. El problema no es que existan, sino que todavia no sabemos gestionarlas bien y convertirlas en un comportamiento comprensible del programa. Eso es justamente lo que haremos a continuacion: aprenderemos a capturar las excepciones esperadas y a seguir trabajando, sin convertir la consola en un teatro dramatico.

1
Tarea
Kotlin SELF, nivel 12, lección 1
Bloqueada
Caja para dos
Caja para dos
1
Tarea
Kotlin SELF, nivel 12, lección 1
Bloqueada
Cupón roto
Cupón roto
1
Tarea
Kotlin SELF, nivel 12, lección 1
Bloqueada
Estante en el almacén
Estante en el almacén
1
Tarea
Kotlin SELF, nivel 12, lección 1
Bloqueada
Ascensor de errores
Ascensor de errores
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION