Tratamento avançado de erros em Kotlin: do try/catch às abordagens funcionais
O tratamento de erros é uma das tarefas essenciais no desenvolvimento de software confiável. Em Kotlin, além do mecanismo clássico try/catch/finally, existe todo um arsenal de ferramentas avançadas: Result, runCatching, sealed class e mônadas funcionais. Neste artigo, vamos explorar como ir além do uso básico e tornar seu código mais seguro, legível e previsível.
1. try/catch clássico: quando ainda é apropriado
O try/catch básico em Kotlin é uma expressão que pode retornar um valor. Essa é a primeira diferença em relação ao Java.
val result: String = try { riskyOperation()} catch (e: IOException) { "Erro de entrada/saída: ${e.message}"} catch (e: Exception) { "Erro desconhecido: ${e.message}"} finally { println("O bloco finally sempre é executado")}Apesar da simplicidade, essa abordagem tem desvantagens:
- Exceções são operações custosas (criação de stack trace).
- O tipo de erro não é fixado no nível dos tipos — é fácil esquecer de tratar.
- O código se torna verboso com muitas operações aninhadas.
Quando usar try/catch?
- Ao trabalhar com recursos externos (arquivos, rede, bancos de dados).
- Em código de bibliotecas, onde exceções fazem parte do contrato.
- Para erros críticos que não podem ser tratados localmente.
2. Abordagem funcional: Result e runCatching
Kotlin oferece a classe embutida Result, que representa o resultado de uma operação que pode ser bem-sucedida ou falhar. É uma alternativa às exceções, sem exigir um stack trace custoso.
fun divide(a: Int, b: Int): Result<Int> { return runCatching { a / b }}
fun main() { val result = divide(10, 0) result.onSuccess { value -> println("Resultado: $value") }.onFailure { error -> println("Erro: ${error.message}") }}Métodos de Result
getOrNull()— retorna o valor ou null.getOrDefault(default)— retorna o valor ou um valor padrão.mapeflatMap— para encadeamento de transformações.recover— recuperação após um erro.
val safeResult = runCatching { riskyOperation() } .map { it.toUpperCase() } .recover { "default_value" } .getOrDefault("fallback")Importante: Result não foi projetado para ser transmitido entre threads ou armazenado em campos de classe — use-o apenas como um valor local.
3. Classes sealed: modelando erros no nível dos tipos
Quando é necessário definir claramente todos os estados possíveis (sucesso, diferentes tipos de erro), as sealed class entram em ação. São uma ferramenta poderosa para criar tipos de dados algébricos.
sealed class NetworkResult<out T> { data class Success<T>(val data: T) : NetworkResult<T>() data class Error(val message: String, val code: Int) : NetworkResult<Nothing>() object Loading : NetworkResult<Nothing>()}
fun fetchUser(id: Int): NetworkResult<User> { return try { val user = api.getUser(id) NetworkResult.Success(user) } catch (e: HttpException) { NetworkResult.Error("O servidor retornou um erro", e.code()) } catch (e: Exception) { NetworkResult.Error("Erro desconhecido", 0) }}
fun handleResult(result: NetworkResult<User>) { when (result) { is NetworkResult.Success -> println("Usuário: ${result.data.name}") is NetworkResult.Er