Saltar al contenido
K
Kotlin: el lenguajePaso a paso25 min de lectura · 3 min de máquina Básico

Excepciones: try, catch y require

Al terminar: Provocar y capturar errores con throw, try, catch y finally, y comprobar condiciones con require, check y error.

Hasta ahora Torneo.agregar(partido) lanza una excepción si algo está mal, y quien la llama tiene que acordarse de capturarla. Esta lección enseña esa herramienta por dentro: qué es una excepción, cómo se atiende y cómo se comprueban las condiciones con una línea. En la lección siguiente, Result: errores como valores, Torneo.registrar(...) devolverá un Result<Partido> en vez de lanzar. El proyecto no cambia en esta: todo se prueba en el Playground.

Temas: lanzar una excepción con throw y atenderla con try/catch/finally, varios catch, try como expresión, y require, check y error.

Excepciones: un fallo que sube hasta que alguien lo atiende

Una excepción es la forma que tiene un programa de decir «aquí ha pasado algo que no puedo resolver». Interrumpe la función donde ocurre y sube por las funciones que la llamaron hasta encontrar un try/catch que la atienda. Si nadie la atiende, el programa termina con un stack trace, como el que viste en Nulabilidad.

Dos palabras de la cita: una subclase es una clase que es un caso más concreto de otra (IllegalArgumentException es un caso más concreto de Exception), y Throwable es el tipo más general de todos: agrupa las excepciones y los errores graves. Casi siempre escribirás excepciones concretas, no Throwable.

Lanzar una excepción y atenderla

Lanzar una excepción es escribir throw seguido de la excepción que quieres crear, con un texto que explique qué pasó. Atenderla es envolver el código que puede fallar en un try y escribir, en el catch, qué hacer si ocurre.

ejemplo mínimo: lanzar y atender una excepción (con comentarios)
fun precioConDescuento(precio: Double, descuento: Int): Double {
    if (descuento !in 0..100) {                       // ¿el descuento es un porcentaje posible?
        throw IllegalArgumentException("Descuento imposible: $descuento")   // throw lanza la excepción
    }
    return precio - precio * descuento / 100
}
 
fun main() {
    try {
        println(precioConDescuento(50.0, 20))         // 40.0: no hay problema y se imprime
        println(precioConDescuento(50.0, 150))        // lanza: la ejecución salta al catch
        println("esta línea nunca se ejecuta")        // la función quedó interrumpida
    } catch (e: IllegalArgumentException) {       // `e` es el nombre que eliges para la excepción; atiende solo este tipo
        println("Fallo: ${e.message}")            // message: el texto que se pasó a throw
    }
    println("el programa sigue")
}

Al ejecutarlo, imprime:

40.0
Fallo: Descuento imposible: 150
el programa sigue

Si quitaras el try/catch, la excepción subiría hasta el final de main y el programa terminaría con un stack trace en lugar de seguir.

try como expresión y finally

En Kotlin, try también es una expresión: su valor es el del bloque que se ejecute (el try si todo va bien, el catch si algo falla). Así puedes guardar en una variable el resultado o un valor de respaldo. El bloque finally, si lo hay, se ejecuta siempre y no cambia el valor.

ejemplo mínimo: try como expresión (con comentarios)
fun cantidadValida(unidades: Int): Int {
    require(unidades in 1..10) { "Se venden de 1 a 10 unidades, no $unidades" }
    return unidades
}
 
fun main() {
    // try es una expresión: su valor es el del bloque que se ejecute.
    val unidades: Int? = try {
        cantidadValida("3".toInt())        // "3" se convierte en 3 y es válida: este es el valor
    } catch (e: NumberFormatException) {
        null
    } finally {
        println("el try terminó")          // finally se ejecuta siempre
    }
    println(unidades)
 
    val otras: Int? = try {
        cantidadValida("diez".toInt())     // "diez" no es un número: lanza NumberFormatException
    } catch (e: NumberFormatException) {
        null                               // este valor es el de todo el try
    } finally {
        println("el try terminó")
    }
    println(otras)
 
    val demasiadas = try {
        cantidadValida(50)                 // lanza IllegalArgumentException y este catch la atiende: el valor es -1
    } catch (e: IllegalArgumentException) {
        -1
    }
    println(demasiadas)
}

Al ejecutarlo, imprime:

el try terminó
3
el try terminó
null
-1

Varios catch

Un try puede llevar varios catch seguidos, uno por cada tipo de excepción, y entra solo el primero que encaja. Sirve para responder distinto a cada fallo. NumberFormatException (el texto no es un número) es un caso más concreto de IllegalArgumentException (un argumento que no vale), y por eso va primero: si fuera al revés, el catch general se llevaría los dos casos.

ejemplo mínimo: dos catch con respuestas distintas (con comentarios)
fun cantidadValida(unidades: Int): Int {
    require(unidades in 1..10) { "Se venden de 1 a 10 unidades, no $unidades" }
    return unidades
}
 
fun descripcion(texto: String): String {
    return try {
        "vale ${cantidadValida(texto.toInt())}"
    } catch (e: NumberFormatException) {    // el texto no es un número
        "no es un número"
    } catch (e: IllegalArgumentException) { // es un número, pero require lo rechazó
        "fuera de rango"
    }
}
 
fun main() {
    println(descripcion("3"))
    println(descripcion("diez"))
    println(descripcion("50"))
}
vale 3
no es un número
fuera de rango

require, check y error

Escribir if (…) throw … cada vez es largo. Tres funciones lanzan la excepción por ti cuando la condición no se cumple:

FunciónPara quéLanza
require(cond) { … }Comprobar lo que te dan (argumentos)IllegalArgumentException
check(cond) { … }Comprobar el estado del objetoIllegalStateException
error("…")Marcar un caso que no debería ocurrirIllegalStateException
ejemplo mínimo: require, check y error (con comentarios)
class Cuenta(var saldo: Int) {
    fun retirar(cantidad: Int) {
        require(cantidad > 0) { "La cantidad debe ser positiva" }     // comprueba lo que te dan
        check(saldo >= cantidad) { "Saldo insuficiente: $saldo" }     // comprueba el estado del objeto
        saldo -= cantidad
    }
}
 
fun nombreDelDia(numero: Int): String = when (numero) {
    1 -> "lunes"
    2 -> "martes"
    else -> error("Día sin definir: $numero")                         // error() siempre lanza y nunca termina con normalidad, así que Kotlin acepta esta rama aunque el when deba dar un texto
}
 
fun main() {
    val cuenta = Cuenta(100)
    try { cuenta.retirar(-5) } catch (e: IllegalArgumentException) { println("require: ${e.message}") }
    try { cuenta.retirar(500) } catch (e: IllegalStateException) { println("check: ${e.message}") }
    try { nombreDelDia(9) } catch (e: IllegalStateException) { println("error: ${e.message}") }
}

Al ejecutarlo, imprime:

require: La cantidad debe ser positiva
check: Saldo insuficiente: 100
error: Día sin definir: 9

Reto · Ejercicio: un minuto que puede no valer

En el Kotlin Playground, fuera de tu proyecto:

  1. Una función minutoDesdeTexto que recibe un texto con un minuto de partido, de 0 a 120, y devuelve ese minuto como entero, o null si el texto no es un número o el número se sale de ese rango. No debe lanzar nunca. El texto puede traer espacios a los lados, y trim() los quita de los dos lados.
  2. Pruébala con "45", "200", "diez" y " 90 " e imprime los cuatro resultados, uno por línea.

Salida esperada:

45
null
null
90
Ver solución
solución del reto
fun minutoDesdeTexto(texto: String): Int? {
    return try {
        val minuto = texto.trim().toInt()                       // lanza NumberFormatException si no es un número
        require(minuto in 0..120) { "Minuto fuera del partido: $minuto" }
        minuto
    } catch (e: NumberFormatException) {
        null
    } catch (e: IllegalArgumentException) {
        null
    }
}
 
fun main() {
    println(minutoDesdeTexto("45"))
    println(minutoDesdeTexto("200"))
    println(minutoDesdeTexto("diez"))
    println(minutoDesdeTexto(" 90 "))
}

Los dos fallos (texto que no es un número y número fuera de rango) se atienden con su propio catch y los dos devuelven null. Como NumberFormatException va primero, el catch general solo recoge lo que quede.

Paso · El proyecto sigue igual

Objetivo. Comprobar que Marcador sigue como lo dejaste. No tocas ningún fichero: en la lección siguiente Torneo.registrar usa estas excepciones por dentro.

Punto de control

  • En la Terminal de Android Studio (View → Tool Windows → Terminal), ./gradlew :torneo:test :torneo:run termina en BUILD SUCCESSFUL.
  • Abre el informe de tests como en Bucles y tu primer test (open torneo/build/reports/tests/test/index.html) y comprueba que arriba pone 65 tests, los mismos de antes: esta lección no añade ninguno.
  • En el Playground, tu ejercicio imprime 45, null, null y 90.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.Dentro de un try, una función ejecuta throw IllegalArgumentException("mal") y hay un catch (e: IllegalArgumentException). ¿Qué ocurre con las líneas que van después del throw, dentro del try?

  2. 2.¿Cuál es el valor de val n = try { "diez".toInt() } catch (e: NumberFormatException) { -1 }?

  3. 3.¿Qué diferencia hay entre require y check?

  4. 4.Un try tiene dos catch: primero NumberFormatException y luego IllegalArgumentException. El bloque lanza una NumberFormatException. ¿Qué catch entra?

Faltan 4 de 4