Introdução: Por que o try/catch padrão não é suficiente?
\n\nO desenvolvimento em Kotlin, especialmente no contexto de aplicações servidoras ou Android, frequentemente enfrenta a necessidade de um tratamento de erros robusto. O bloco padrão try/catch é a base, mas na prática ele leva a uma lógica "espalhada" e dificulta os testes. Em um nível avançado, não queremos apenas capturar exceções, mas sim projetar o sistema de forma que os erros sejam parte do tipo de dado, e não um fluxo de controle inesperado.
Neste artigo, vamos explorar como ir além do try/catch e usar abordagens funcionais: Result, Either (de bibliotecas) e sealed class. Você aprenderá como tornar o código previsível, type-safe e fácil de manter.
1. try/catch clássico: quando ainda é útil?
\n\nNão devemos abandonar completamente o try/catch. Ele é indispensável para trabalhar com código legado, bibliotecas externas (por exemplo, ao fazer parsing de JSON) e chamadas de sistema (entrada/saída). No entanto, existem nuances importantes.
Estrutura correta e recursos
\nEm Kotlin, ao contrário do Java, não há exceções verificadas, mas há a elegante construção use para fechamento automático de recursos:
fun readFile(path: String): String? = try {\n BufferedReader(FileReader(path)).use { it.readText() }\n} catch (e: IOException) {\n println("Erro ao ler arquivo: ${e.message}")\n null\n}\n\nAqui, use garante o fechamento do arquivo, mesmo que uma exceção ocorra internamente. Esta é uma substituição elegante para o try-with-resources.
Problema: perda de contexto
\nA principal desvantagem do try/catch é que ele não informa ao código chamador que a função pode "falhar". A exceção é um efeito colateral oculto. A chamada readFile() pode retornar null ou lançar uma IOException, mas a assinatura da função não reflete isso. Isso leva a erros em tempo de execução.
A solução é migrar para uma representação explícita de erros.
\n\n2. Result: a mônada embutida para sucesso ou falha
\n\nKotlin fornece a classe padrão Result<T>. Ela é uma estrutura algébrica simples: ou sucesso (valor T), ou falha (Throwable). Isso torna o tratamento de erros parte do sistema de tipos.
Uso básico
\nfun divisaoArriscada(a: Int, b: Int): Result<Int> = runCatching {\n a / b\n}\n\nfun main() {\n val resultado = divisaoArriscada(10, 0)\n resultado.onSuccess { println("Resultado: $it") }\n .onFailure { println("Erro: ${it.message}") }\n}\n\nrunCatching é um invólucro sobre try/catch que retorna um Result. Os métodos onSuccess e onFailure permitem reagir ao resultado sem interromper a cadeia de chamadas.
Transformação e encadeamento
\nVocê pode transformar o resultado sem se preocupar com exceções dentro do map:
\n\nval resultadoFinal = divisaoArriscada(10, 2)\n .map { it * 2 } // se sucesso — multiplica\n .recover { erro -> // se erro — retorna um valor padrão\n println("Usando valor padrão devido a: ${erro.message}")\n 0\n }\nprintln(resultadoFinal.getOrDefault(-1)) // 10\n\nImportante: Result não foi projetado para ser armazenado em coleções ou transmitido entre threads sem processamento adicional. Para esses fins, sealed class ou bibliotecas como Arrow são mais adequadas.
3. Either: diferenciando o tipo de erro
\n\nA classe Result captura apenas Throwable. Mas frequentemente queremos diferenciar tipos de erros: "não encontrado", "permissão insuficiente", "erro de validação". É aqui que entra o Either<L, R> de bibliotecas funcionais (por exemplo, Arrow ou implementações próprias