A borda e os erros
Se o programa é só uma descrição até você rodá-lo, uma pergunta natural aparece: então quando, exatamente, o banco é acessado, o arquivo é lido, o HTTP é feito? A resposta organiza a arquitetura de uma aplicação Effect: os efeitos colaterais acontecem na borda, no instante em que o runtime interpreta a descrição, e não onde o código foi escrito.
Localização no código não é o mesmo que execução
Section titled “Localização no código não é o mesmo que execução”Vale desfazer uma confusão fácil entre onde uma operação está escrita e onde ela roda. Imagine três arquivos: o main.ts monta o programa, um service.ts usa um repositório, e o repository.ts descreve a consulta ao banco.
export const findUser = (id: string) => Effect.tryPromise({ try: () => sql.query(/* ... */), catch: (e) => new DbError({ e }) })Quando esse módulo é importado, o sql.query não é chamado. O que se cria é um Effect que sabe como chamar sql.query quando chegar a hora. Lá no main.ts, você faz:
Effect.runPromise(program)e o runtime começa a percorrer a árvore. Em algum momento ele chega ao nó findUser, definido no repository.ts, e é nesse instante que o callback () => sql.query(...) executa. A consulta ao banco acontece naquele ponto da interpretação, não “dentro do main.ts”. O main.ts apenas iniciou a caminhada.
A analogia que fecha a ideia é a de um interpretador de bytecode. Você carrega o programa inteiro na memória, o interpretador começa no ponto de entrada e vai seguindo as instruções. Algumas foram definidas em arquivos diferentes, mas isso é irrelevante na hora de rodar: o interpretador só percorre o programa e executa cada instrução ao alcançá-la. O runtime do Effect faz o mesmo, e quando encontra uma folha de fronteira (um Effect.tryPromise, um Effect.sync), executa a operação concreta que ela descreve.
Então “os efeitos acontecem na borda” não é uma afirmação sobre o arquivo onde o código mora. É arquitetural: dentro da lógica de negócio, as funções descrevem efeitos; na borda (o main, um handler HTTP, um worker, um cron, um consumidor de fila), você chama run e entrega a descrição ao runtime. Desse ponto em diante, o mundo impuro começa, e o runtime executa cada efeito conforme percorre a árvore.
Falhas são valores, não exceções
Section titled “Falhas são valores, não exceções”A borda também é onde o modelo de erros do Effect se separa do modelo da linguagem. No mundo JavaScript, uma falha é uma exceção: você dá throw e alguém, mais acima, precisa lembrar de dar catch. No mundo Effect, uma falha é um valor tipado, que viaja no canal E do Effect<A, E, R> e que o compilador não deixa você ignorar.
A ponte entre os dois mundos são os construtores de fronteira. Código que pode lançar é embrulhado por Effect.try (síncrono) ou Effect.tryPromise (assíncrono), e a exceção da plataforma é traduzida ali para uma falha do seu domínio:
// em vez de try { ... } catch (e) { throw new DatabaseError(e) }Effect.tryPromise({ try: () => sql.query(/* ... */), catch: (e) => new DbError({ cause: e }),})Depois dessa tradução, o resto da aplicação não usa mais try/catch da linguagem. O try/catch fica concentrado nas bordas, onde se conversa com APIs que dão throw (o JSON.parse, o fs.readFileSync, drivers de banco, SDKs de terceiros), e o fluxo segue tratando a falha como dado, com combinadores como catchTag, catchAll e orElse.
sync contra try: onde o erro nasce tipado
Section titled “sync contra try: onde o erro nasce tipado”O par de construtores tem uma escolha que parece trivial e não é. Use Effect.sync para código que você sabe que não lança, e Effect.try para código que pode lançar. O JSON.parse claramente pode lançar, então pede try.
O interessante é o que acontece quando você erra a escolha. Se você escreve Effect.sync(() => JSON.parse(text)) e o parse falha, o runtime não deixa o processo explodir: ele captura a exceção mesmo assim e a transforma numa falha. O problema é que tipo de falha. Como você prometeu, via sync, que aquilo não lançaria, o Effect não tem para onde traduzir a exceção, e ela vira um defeito (um defect, uma falha inesperada e não tipada), fora do canal E. Você perde a chance de dizer “quando o parse falhar, isso é um ParseError que o resto do código sabe tratar”.
// ❌ promete que não lança; se lançar, vira defeito genérico, fora do canal Econst a = Effect.sync(() => JSON.parse(text))
// ✅ admite que pode lançar; a falha entra no canal E como ParseErrorconst b = Effect.try({ try: () => JSON.parse(text), catch: (e) => new ParseError({ e }) })Modelar e tratar o erro
Section titled “Modelar e tratar o erro”Do lado da modelagem, o idiomático é não usar throw new Error() e sim representar o erro como um dado com uma tag discriminante, via Data.TaggedError:
import { Data } from "effect"
class UserNotFound extends Data.TaggedError("UserNotFound")<{ readonly id: string }> {}A tag é o que permite tratar o erro por nome, com exaustividade verificada pelo compilador. O catchTag intercepta um erro específico do canal E e o resolve, removendo-o do tipo:
program.pipe( Effect.catchTag("UserNotFound", (e) => Effect.succeed(`usuário ${e.id} não existe`)),)O catálogo completo (catchTags, catchAll, mapError, orElse, Effect.either) e a distinção entre Data.TaggedError e Schema.TaggedError para erros que cruzam a rede ficam em Effect essencial. O ponto conceitual aqui é que, tendo o erro como valor tipado, tratá-lo é escolher um combinador, não montar um try/catch.
E a stack trace?
Section titled “E a stack trace?”Uma dúvida comum: se o erro estoura numa folha lá no repository.ts, a stack aponta para o repository.ts ou para o main.ts que chamou run? Aponta para o lugar de verdade. O main.ts só iniciou a interpretação; a exceção nasce no callback que roda sql.query, então a stack física sobe do repository.ts para cima, com o runtime e o main.ts aparecendo apenas como o ponto de entrada da execução.
A diferença em relação ao JavaScript cru é que o Effect captura essa exceção antes que ela derrube o processo e a converte numa falha tratável. E, com observabilidade ligada (tracing habilitado), o runtime ainda pode enriquecer o diagnóstico com uma stack lógica do programa, mostrando por quais efeitos e fibras a execução passou, além da stack tradicional do JavaScript. É uma vantagem de ter um runtime próprio em vez de só deixar exceções se propagarem.
Palavras-chave
Section titled “Palavras-chave”Conceitos: borda, runtime como interpretador, localização vs. execução, defeito (defect), falha tipada, canal de erro, stack lógica, tracing
Construtores: Effect.sync, Effect.try, Effect.promise, Effect.tryPromise
Tratamento: Data.TaggedError, catchTag, catchAll, orElse, tag discriminante
Contraste: throw/catch da linguagem vs. Effect.fail, exceção vs. valor
Analogias: interpretador de bytecode, ponto de entrada