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árioAbordagem recomendada
Erro de programação (bug)Exceção (IllegalStateException)
Erro de domínio esperadoEither / Raise
Validação de entradazipOrAccumulate
Ausência de valorKotlin nullable (?) ou Option
Erro de infraestruturaExceçã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:

  1. Comece pela camada de serviço — substitua exceções de domínio por Either
  2. Adote Raise DSL nos novos serviços
  3. Use zipOrAccumulate em validações de formulários e APIs
  4. 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.