Se você programa em Kotlin e já sentiu que o tratamento de erros com exceções é frágil, imprevisível e difícil de compor, a biblioteca Arrow pode mudar completamente sua perspectiva. Arrow é a principal biblioteca de programação funcional para Kotlin — e em 2026, com a Raise DSL e integração nativa com coroutines, ela está mais acessível e poderosa do que nunca.
O que é Arrow?
Arrow é uma biblioteca open-source que traz conceitos de programação funcional tipada para Kotlin. Diferente de abordagens acadêmicas, Arrow foi projetada para ser prática — ela resolve problemas reais de tratamento de erros, composição de operações e modelagem de domínio.
Os pilares principais são:
- Either — representar sucesso ou falha sem exceções
- Option — lidar com ausência de valor de forma explícita
- Raise DSL — a abordagem moderna para error handling composável
- Validated — acumular múltiplos erros em vez de parar no primeiro
Arrow não é um framework que exige que você reescreva tudo. Você pode adotar uma peça de cada vez, no ritmo do seu projeto.
Configurando Arrow no projeto
Adicione ao seu build.gradle.kts:
dependencies {
implementation("io.arrow-kt:arrow-core:1.2.4")
implementation("io.arrow-kt:arrow-fx-coroutines:1.2.4")
}
A dependência arrow-core traz Either, Option e Raise. Já arrow-fx-coroutines adiciona integração com coroutines do Kotlin para operações assíncronas.
Either — Erros sem exceções
O problema com exceções é que elas são invisíveis na assinatura da função. Quando você chama buscarUsuario(id), não tem como saber, olhando a assinatura, que ela pode lançar UsuarioNaoEncontradoException. Isso leva a bugs em produção.
Either resolve isso tornando o erro explícito no tipo de retorno:
import arrow.core.Either
import arrow.core.left
import arrow.core.right
sealed class ErroUsuario {
data class NaoEncontrado(val id: Long) : ErroUsuario()
data class EmailInvalido(val email: String) : ErroUsuario()
}
data class Usuario(val id: Long, val nome: String, val email: String)
fun buscarUsuario(id: Long): Either<ErroUsuario, Usuario> {
val usuario = repository.findById(id)
return if (usuario != null) {
usuario.right()
} else {
ErroUsuario.NaoEncontrado(id).left()
}
}
Agora, quem chama buscarUsuario sabe que a função pode falhar e é obrigado a lidar com isso:
when (val resultado = buscarUsuario(42L)) {
is Either.Left -> println("Erro: ${resultado.value}")
is Either.Right -> println("Encontrado: ${resultado.value.nome}")
}
A grande vantagem é a composição. Com map, flatMap e fold, você encadeia operações sem perder o controle:
fun processarPedido(usuarioId: Long): Either<ErroUsuario, Pedido> {
return buscarUsuario(usuarioId)
.map { usuario -> criarPedido(usuario) }
.flatMap { pedido -> validarPedido(pedido) }
}
Se qualquer etapa falhar, o erro é propagado automaticamente — sem try/catch, sem exceções voando.
Raise DSL — A abordagem moderna
A partir do Arrow 1.2, a Raise DSL se tornou a forma recomendada de trabalhar com erros. Ela combina a segurança do Either com uma sintaxe imperativa que parece código “normal”:
import arrow.core.raise.either
import arrow.core.raise.Raise
import arrow.core.raise.ensure
fun Raise<ErroUsuario>.buscarUsuario(id: Long): Usuario {
val usuario = repository.findById(id)
ensure(usuario != null) { ErroUsuario.NaoEncontrado(id) }
return usuario
}
fun Raise<ErroUsuario>.validarEmail(email: String): String {
ensure(email.contains("@")) { ErroUsuario.EmailInvalido(email) }
return email
}
fun Raise<ErroUsuario>.criarUsuario(nome: String, email: String): Usuario {
val emailValidado = validarEmail(email)
val usuario = Usuario(0L, nome, emailValidado)
return repository.salvar(usuario)
}
O bloco either { } converte o resultado para Either:
val resultado: Either<ErroUsuario, Usuario> = either {
val usuario = buscarUsuario(42L)
val atualizado = usuario.copy(nome = "Novo Nome")
repository.salvar(atualizado)
}
O código dentro do either { } parece imperativo — sem map, flatMap ou encadeamento manual. Mas a segurança é a mesma: se qualquer função com Raise falhar, a execução é interrompida e o erro é retornado como Either.Left.
Essa é a grande inovação do Raise: você escreve código que parece procedural mas é funcional.
Option — Ausência explícita de valor
Kotlin já tem null safety com ?, mas Option do Arrow ainda tem seu lugar quando você quer uma API mais funcional e composável:
import arrow.core.Option
import arrow.core.Some
import arrow.core.None
import arrow.core.toOption
fun buscarConfig(chave: String): Option<String> {
return System.getenv(chave).toOption()
}
val porta = buscarConfig("PORT")
.map { it.toInt() }
.getOrElse { 8080 }
Na prática, muitos projetos Kotlin preferem usar ? diretamente. Mas Option brilha quando você precisa de interoperabilidade com outras construções do Arrow ou quando trabalha com coleções de valores opcionais.
Acumulando erros com Validated e zipOrAccumulate
Um caso comum é validação de formulários: você quer mostrar todos os erros de uma vez, não parar no primeiro. O Arrow oferece zipOrAccumulate para isso:
import arrow.core.raise.either
import arrow.core.raise.zipOrAccumulate
data class RegistroUsuario(val nome: String, val email: String, val idade: Int)
fun validarRegistro(
nome: String,
email: String,
idade: Int
): Either<List<String>, RegistroUsuario> = either {
zipOrAccumulate(
{ ensure(nome.isNotBlank()) { "Nome não pode estar vazio" } },
{ ensure(email.contains("@")) { "Email inválido" } },
{ ensure(idade >= 18) { "Deve ter pelo menos 18 anos" } }
) { _, _, _ ->
RegistroUsuario(nome, email, idade)
}
}
Se o nome estiver vazio e o email for inválido e a idade for menor que 18, todos os três erros são retornados juntos. Isso é impossível de fazer elegantemente com exceções.
Arrow e coroutines
Arrow se integra perfeitamente com coroutines do Kotlin. Operações assíncronas podem ser compostas mantendo o tratamento de erros funcional:
import arrow.fx.coroutines.parZip
suspend fun carregarDashboard(userId: Long): Either<Erro, Dashboard> = either {
parZip(
{ buscarPerfil(userId).bind() },
{ buscarPedidos(userId).bind() },
{ buscarNotificacoes(userId).bind() }
) { perfil, pedidos, notificacoes ->
Dashboard(perfil, pedidos, notificacoes)
}
}
O parZip executa as três operações em paralelo e combina os resultados. Se qualquer uma falhar, o erro é propagado e as outras são canceladas. É concorrência estruturada com error handling funcional — o melhor dos dois mundos. Se você trabalha com Python e asyncio, vai notar que o parZip oferece uma experiência mais segura que o asyncio.gather, já que o sistema de tipos garante o tratamento de erros em tempo de compilação.
Por que Arrow em 2026?
O ecossistema Kotlin está amadurecendo rapidamente. Em 2026, vemos Arrow sendo adotado por equipes que buscam:
- Código mais previsível — sem surpresas de exceções não tratadas
- Melhor testabilidade — funções puras com Either são fáceis de testar (veja nosso guia de testes com JUnit 5 e MockK)
- Composição natural — encadear operações que podem falhar sem aninhamento de try/catch
- Interoperabilidade — Arrow funciona com Spring Boot, Ktor, Android e qualquer projeto Kotlin
Se você já usa DSLs em Kotlin, a Raise DSL do Arrow vai parecer completamente natural. A abordagem é a mesma: usar os recursos da linguagem para criar APIs expressivas e seguras.
Arrow vs. exceções — Quando usar cada um
Arrow não é para substituir todas as exceções. Aqui está uma diretriz prática:
| Cenário | Abordagem recomendada |
|---|---|
| Erro de programação (bug) | Exceção (IllegalStateException) |
| Erro de domínio esperado | Either / Raise |
| Validação de entrada | zipOrAccumulate |
| Ausência de valor | Kotlin nullable (?) ou Option |
| Erro de infraestrutura | Exceção ou Either (depende do contexto) |
A regra geral: se o chamador precisa lidar com o erro como parte do fluxo normal, use Either. Se é um bug que não deveria acontecer, use exceções.
Começando com Arrow gradualmente
Você não precisa adotar Arrow em todo o projeto de uma vez. Uma estratégia eficiente:
- Comece pela camada de serviço — substitua exceções de domínio por Either
- Adote Raise DSL nos novos serviços
- Use zipOrAccumulate em validações de formulários e APIs
- Integre com coroutines para operações assíncronas
Cada passo traz benefícios imediatos sem exigir uma reescrita completa.
Conclusão
Arrow transformou a forma como desenvolvedores Kotlin lidam com erros e efeitos colaterais. Com a Raise DSL, a barreira de entrada caiu drasticamente — você não precisa ser um expert em programação funcional para se beneficiar. O código fica mais seguro, mais composável e mais fácil de testar.
Se você está construindo aplicações Kotlin que precisam de robustez no tratamento de erros — seja backend com Spring Boot, APIs com Ktor ou apps Android — Arrow merece um lugar no seu toolbox. Rust também abraça paradigmas funcionais com Result types e pattern matching, e Go adota retorno explícito de erros como padrão da linguagem — ambas abordagens que inspiram o uso de Either no Kotlin. Comece com Either, evolua para Raise, e descubra como programação funcional pode ser prática e idiomática em Kotlin.