Manejo de errores en Kotlin: try/catch y más allá — de la A a la Z
El manejo de errores es una de las tareas clave de cualquier desarrollador. En Kotlin, como en otros lenguajes modernos, existen varios enfoques para gestionar situaciones excepcionales. En este artículo, analizaremos todos los métodos principales: desde el clásico try/catch hasta técnicas avanzadas con Result, sealed class y Nothing. Aprenderá a escribir código robusto y legible que maneje los errores correctamente.
1. Bloque clásico try/catch/finally
El mecanismo básico de manejo de excepciones en Kotlin es el bloque try/catch. Funciona igual que en Java, pero con algunas mejoras. En Kotlin, try es una expresión que puede devolver un valor.
fun parseNumber(input: String): Int? { return try { input.toInt() } catch (e: NumberFormatException) { println("Error de análisis: ${e.message}") null // devolvemos null en caso de error } finally { println("El bloque finally siempre se ejecuta") }}
fun main() { val result = parseNumber("123abc") println(result) // null}Características clave:
tryse puede usar como expresión: el resultado de la última expresión en el bloquetryocatchse convierte en el resultado de todo el bloque.- El bloque
finallyse ejecuta en cualquier caso (incluso si hubo una excepción o unreturn). - Kotlin no tiene excepciones verificadas (checked exceptions) como Java, lo que simplifica el código.
Es importante recordar que abusar de try/catch hace que el código sea menos legible. Úselo solo para situaciones realmente excepcionales, no para controlar el flujo.
2. Tipo Result y enfoque funcional
Para un manejo de errores más elegante, Kotlin ofrece el tipo Result<T>. Es una clase sealed que puede ser Success (con un resultado) o Failure (con una excepción). Es ideal para el estilo funcional.
import kotlin.Result
fun divide(a: Int, b: Int): Result<Int> { return try { Result.success(a / b) } catch (e: ArithmeticException) { Result.failure(e) }}
fun main() { val result = divide(10, 0) // Manejo con when when (result) { is Result.Success -> println("Resultado: ${result.value}") is Result.Failure -> println("Error: ${result.exception.message}") } // O mediante métodos result.onSuccess { value -> println("Éxito: $value") } .onFailure { e -> println("Fallo: ${e.message}") } // Transformación val mapped = result.map { it * 2 } // si es failure, seguirá siendo failure println(mapped)}Ventajas de Result:
- Muestra explícitamente que la función puede fallar.
- Permite encadenar llamadas (
map,flatMap,recover). - No requiere
try/catchen el código que llama.
Sin embargo, Result no está diseñado para almacenarse en colecciones ni para transmitirse a través de los límites de las corrutinas; para eso es mejor usar sealed class.
3. Clases sealed para el manejo detallado de errores
Cuando necesita manejar diferentes tipos de errores de manera distinta, sealed class es la opción ideal. Puede crear una jerarquía de errores y manejarlos en un when con cobertura exhaustiva.
sealed class NetworkResult<out T> { data class Success<T>(val data: T) : NetworkResult<T>() data class NetworkError(val code: Int, val message: String) : NetworkResult<Nothing>() data class Timeout(val timeout