Skip to content

Effect essencial: o dialeto reduzido

Objetivo: cobrir a maior parte do dia a dia com o menor conjunto possível de APIs. Tudo que não estiver aqui entra na categoria “aprendo quando precisar”.

O Effect acumulou várias formas de fazer a mesma coisa ao longo das versões 1.x → 2.x → 3.x. Este dialeto reduzido escolhe uma forma idiomática por problema e ignora o resto até precisar. Esta página é essa escolha, o complemento prático das páginas conceituais da seção.

Effect<A, E, R> é uma descrição (não uma execução) de um programa que:

  • produz um valor de sucesso do tipo A,
  • pode falhar com um erro tipado E,
  • precisa de dependências (requirements) R para rodar.

Nada acontece até você rodar o Effect (runPromise/runFork, ou o BunRuntime.runMain no entrypoint). Enquanto não roda, é só um valor imutável que você compõe. Pense nele como uma “receita”. O porquê disso está em a descrição e a execução.

Os três canais (A sucesso, E erro, R dependências) são o que diferencia Effect de uma Promise, que só tem “deu certo” e “deu ruim (any)”.

Para qualquer código sequencial de negócio, use Effect.gen. Cada yield* “desembrulha” um Effect (equivalente a um await), mas mantendo erros e dependências tipados.

import { Effect } from "effect"
const program = Effect.gen(function* () {
const user = yield* getUser(id) // yield* = "await" tipado
const posts = yield* getPosts(user.id)
return { user, posts }
})

Regra prática: gen para orquestração; pipe só para transformações curtas.

import { pipe, Effect } from "effect"
const doubled = pipe(
Effect.succeed(21),
Effect.map((n) => n * 2)
)

Esqueça flatMap/zip/Do/andThen como estilo padrão de negócio. Eles funcionam, não estão errados, mas produzem código mais difícil de ler e revisar. gen é o consenso da comunidade para lógica sequencial (ver generators e pipe).

Nuance honesta: a doc oficial chama generators de “opcional”. Não é proibido usar pipe/flatMap, só é menos ergonômico. A heurística “gen para negócio, pipe para transformação” é a que menos gera discussão em code review.

Criar Effects a partir de valores e de código existente

Section titled “Criar Effects a partir de valores e de código existente”
import { Effect } from "effect"
Effect.succeed(42) // Effect<number> valor pronto
Effect.fail(new MyError()) // Effect<never, MyError> falha tipada
Effect.sync(() => Date.now()) // computação síncrona que NÃO lança
Effect.suspend(() => expensive()) // adia a construção do Effect

Integrando Promises (o ponto que mais confunde)

Section titled “Integrando Promises (o ponto que mais confunde)”
// ✅ tryPromise: a Promise PODE rejeitar → você tipa o erro
const res = Effect.tryPromise({
try: () => fetch(url),
catch: (cause) => new NetworkError({ cause }),
})
// tipo: Effect<Response, NetworkError>
// ⚠️ promise: você GARANTE que nunca rejeita. Se rejeitar, vira "defect" (crash)
const now = Effect.promise(() => Promise.resolve(Date.now()))
// tipo: Effect<number, never>

Use tryPromise por padrão. Só use promise quando a rejeição for logicamente impossível. O mesmo par existe para código síncrono: Effect.try({ try, catch }) vs Effect.sync. É assim que se embrulha qualquer client externo que devolve Promise (um driver de banco, um SDK de terceiros). A diferença entre sync e try, e por que ela decide se o erro nasce tipado, está em a borda e os erros.

Não use throw new Error(). Modele erros como dados, com uma tag discriminante.

import { Data } from "effect"
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string
}> {}
return yield* Effect.fail(new UserNotFound({ id }))

A tag permite tratar o erro por nome, com exaustividade verificada pelo compilador:

program.pipe(
Effect.catchTag("UserNotFound", (e) =>
Effect.succeed(`usuário ${e.id} não existe`)
)
)
  • catchTag / catchTags: trata erros específicos por tag (o idiomático).
  • catchAll: trata qualquer erro do canal E.
  • mapError: transforma o erro sem tratá-lo.
  • orElse / Effect.either: degrada para um valor ou converte E em Either.

Distinção que importa na camada HTTP: use Data.TaggedError para erros internos (não serializados) e Schema.TaggedError para erros que cruzam a rede (precisam ser encodados/decodados). Mesmo padrão, um leva Schema junto.

Dependências: Context.Tag + Layer (visão de 30 s)

Section titled “Dependências: Context.Tag + Layer (visão de 30 s)”

A injeção de dependências tem uma página dedicada. O resumo:

import { Effect, Context } from "effect"
// 1. Declarar o serviço (a "interface" + a "chave" de DI) via CLASSE
class Clock extends Context.Tag("Clock")<Clock, {
readonly now: Effect.Effect<number>
}>() {}
// 2. Usar no negócio: o requirement R aparece sozinho no tipo
const program = Effect.gen(function* () {
const clock = yield* Clock
return yield* clock.now
})

Não use Context.GenericTag<T>("Nome"). É a forma genérica de baixo nível. A forma idiomática é a classe acima (ou Effect.Service, ver Layers).

Leia env vars / config de forma tipada e validada. Um Config<A> já é um Effect, então basta yield*.

import { Effect, Config } from "effect"
const load = Effect.gen(function* () {
const port = yield* Config.integer("PORT") // inteiro
const host = yield* Config.string("HOST")
const debug = yield* Config.boolean("DEBUG").pipe( // com default
Config.withDefault(false)
)
return { port, host, debug }
})

APIs úteis: Config.string, Config.integer, Config.number (float), Config.boolean, Config.withDefault, Config.all, Config.nested, Config.redacted (segredos, ex.: DATABASE_URL).

Config.unwrap NÃO lê valor. Ela constrói um Config a partir de um objeto envolto em Wrap<A>, o oposto do que parece. Para ler, use yield* Config.integer(...). ❌ Não use process.env direto.

Recursos com ciclo de vida: acquireRelease + scoped

Section titled “Recursos com ciclo de vida: acquireRelease + scoped”

Para qualquer coisa que abre e precisa fechar (conexão, arquivo, socket), pareie aquisição e liberação. O release roda sempre, mesmo em erro ou interrupção.

import { Effect } from "effect"
const dbConnection = Effect.acquireRelease(
Effect.sync(() => openConnection()), // acquire
(conn) => Effect.sync(() => conn.close()) // release (sempre roda)
)
const program = Effect.gen(function* () {
const conn = yield* dbConnection
return yield* conn.query("SELECT 1")
}).pipe(Effect.scoped)

Na prática, recursos de infra (o pool do banco) são adquiridos dentro de um Layer.scoped (ver Layers), e o BunRuntime.runMain fecha o escopo no shutdown.

import { Effect } from "effect"
Effect.all([taskA, taskB, taskC], { concurrency: "unbounded" }) // coletar todos
Effect.forEach(ids, (id) => getUser(id), { concurrency: 10 }) // map efetivo
const fiber = yield* Effect.fork(longTask) // background

Por padrão tudo é sequencial. Você opta pela concorrência com { concurrency } (número | "unbounded" | "inherit"). Se um falha, os outros são interrompidos automaticamente. É concorrência estruturada de graça.

import { Effect } from "effect"
const cached = yield* Effect.cached(expensiveEffect) // indefinido
const withTtl = yield* Effect.cachedWithTTL(expensiveEffect, "5 minutes")
const getUserCached = yield* Effect.cachedFunction((id: string) => getUser(id))

As três existem, são atuais e idiomáticas. Substituem memoização manual com Map.

No backend você precisa de pouquíssimas variantes:

import { Effect } from "effect"
Effect.runPromise(program) // Effect<A> → Promise<A> (rejeita em erro)
Effect.runFork(program) // roda em um fiber, retorna o handle

No entrypoint do servidor, o ponto de entrada é BunRuntime.runMain(...), que cuida do ciclo de vida, dos sinais de shutdown e da finalização dos recursos scoped. runPromiseExit é útil em testes, porque retorna o Exit completo sem lançar.

O subconjunto de módulos que você realmente usa

Section titled “O subconjunto de módulos que você realmente usa”
MóduloPara quê
Effecto core: criar, compor, rodar
Layermontar o grafo de dependências
Contextdeclarar serviços (tags)
Schemavalidação, parse/encode, contratos HTTP
Configconfiguração tipada
Dataerros e estruturas com igualdade
Durationtempos ("5 minutes", Duration.seconds(30))
Optionvalor opcional sem null
Eithersucesso/erro síncrono
Arrayhelpers imutáveis
Consolelogging simples

”Aprenda quando precisar” (evite no começo)

Section titled “”Aprenda quando precisar” (evite no começo)”

Stream, Channel, Sink · STM · Mailbox, Queue, Hub/PubSub · Deferred · FiberRef · Schedule avançado · Runtime customizado. Só puxe um destes quando tiver um problema concreto que o exige.

  • Produção → use Effect v3.x. É a versão oficialmente recomendada.
  • Effect v4 existe (beta desde 18/02/2026, ainda em beta) e consolida APIs duplicadas: por exemplo, os quatro jeitos de declarar serviço (Context.Tag, Context.GenericTag, Effect.Tag, Effect.Service) colapsam num único Context.Service. Escrever no dialeto reduzido hoje é o caminho de menor atrito para essa migração futura.
  • Os módulos HTTP (HttpApi, HttpClient) do @effect/platform são oficialmente “Unstable”: excelentes e recomendados, mas a superfície pode mudar entre releases minor. Vale assumir isso ao construir sobre eles.

Conceitos: dialeto reduzido, descrição, três canais, erro tipado, concorrência estruturada, memoização, ciclo de vida, defect Core: Effect.gen, yield*, pipe, Effect.succeed, Effect.fail, Effect.sync, Effect.suspend Promises: Effect.tryPromise, Effect.promise, Effect.try Erros: Data.TaggedError, Schema.TaggedError, catchTag, catchAll, mapError, orElse, Effect.either Configuração: Config.string, Config.integer, Config.redacted, Config.withDefault Recursos e concorrência: acquireRelease, Effect.scoped, Effect.all, Effect.forEach, Effect.fork Rodar: runPromise, runFork, runPromiseExit, BunRuntime.runMain Módulos: Effect, Layer, Context, Schema, Config, Data, Duration, Option, Either Versões: v3.x, v4 beta, @effect/platform