name: effect-doctor
description: Diagnose and improve Effect-TS code against canonical patterns. Use whenever the user is writing, reviewing, refactoring, or debugging code that imports from effect, @effect/*, or a project built on Effect (like effectctx). Covers Layer composition, Service/Context.Tag, error channels with Data.TaggedError, Scope and resource management, Stream/Queue/PubSub, Schema decoding at boundaries, fiber management, tracing with withSpan, and the anti-patterns that experienced Effect users keep flagging (missing yield*, unbounded concurrency, providing Layers twice, accessor R-leakage, etc.). Trigger on phrases like "review my Effect code", "is this idiomatic Effect", "audit the Layer composition", "why is this fiber not interrupting", "what's the right error type here", "convert this Promise code to Effect", or any task that involves writing non-trivial Effect-TS code where idiom matters.
effect-doctor
You are diagnosing or writing Effect-TS code. The goal is idiomatic Effect — the way the core team and production adopters actually structure things — not "TypeScript with Effect sprinkled on top."
How to use this skill
Two modes:
- Writing new Effect code — use the patterns below as the default vocabulary. Reach for
references/anti-patterns.mdwhen you catch yourself drifting. - Auditing existing Effect code — walk the checklist in
references/audit-checklist.mdagainst the diff or files in scope. Flag findings with concrete file:line references. Don't rewrite anything until the user has seen the findings.
When in doubt about which canonical primitive applies, consult references/primitives.md — it has the decision tables (Layer.effect vs scoped vs scopedDiscard; fail vs die; fork vs forkScoped vs forkDaemon; Stream vs Queue vs PubSub).
The load-bearing rules
These are the rules that, when broken, produce the bugs experienced Effect users see over and over. Internalize them; everything else is style.
Layers
Layer.scopedwhenever construction usesacquireReleaseoraddFinalizer. OtherwiseLayer.effect.Layer.scopedDiscardis for environment mutations with no service surface (installing aFiberRefpatch, registering a long-lived finalizer, swappingClock/ConfigProvider).- Provide each Layer exactly once, at the entry boundary. Providing the same Layer in two places gives two instances. Use
ManagedRuntime.make(layer)to hand a runtime to non-Effect hosts. - Many small Layers, composed at the top. Avoid god-layers. The
@effect/platformsplit (abstract / shared / runtime-specific) is the reference shape. - Tag identifier convention:
"@scope/pkg/ServiceName". Stable across HMR and bundlers.
Services
Effect.Serviceclass form is the modern default.Context.Tagis still first-class.- Default to
accessors: falseon public library APIs.accessors: truepropagates the service's ownRinto every caller, which looks like dependency leakage in inferred types. Use plain functions that take the service viaEffect.geninternally when shipping a library. - Ship both the service class and a default
LiveLayer. Real test implementations beat mocks.
Errors
Data.TaggedError("Foo")<{...}>is the primary tool. Use it for the error channel of every public function.Schema.TaggedErrorwhen errors cross a wire (workers, RPC, SSE, persisted event log). PlainData.TaggedErrordoesn't serialize.Effect.failvsEffect.die:failfor things callers should handle (typed inE).diefor invariant violations. Defects don't appear inE; onlycatchAllCause/sandboxsees them.- Keep
Eprecise and small at boundaries. If internals have many error types, catch and re-tag into 2-4 documented public errors.
Resource management
- Inside services, prefer
Effect.acquireReleasefor local acquire/release pairs. UseEffect.addFinalizerfor cross-cutting cleanup tied to the surrounding scope. - Finalizers run in reverse order and receive an
Exit, so they can branch on success/failure/interrupt. - Per-request work inside a long-running runtime: wrap the unit in
Effect.scopedso resources don't accumulate on the parent scope.
Streams, Queues, PubSub
- Single result:
Effect. Zero-or-more values over time:Stream. Queueis single-consumer with back-pressure.PubSubis multi-consumer broadcast (useStream.fromPubSub(hub)per subscriber). For an event log with multiple subscribers (UI, persistence, eval),PubSubis the right primitive.- Choose the back-pressure strategy explicitly:
bounded,dropping,sliding,unbounded. The default isn't always what you want.
Schema
- Decode at the boundary, encode at the boundary, internal code uses the decoded type. This is the load-bearing rule. Don't pass
Schema.encodedSchemashapes through business logic. - Round-trip invariant:
encode . decode === id. If a schema breaks this, it's a bug in the schema. - For libraries, export schemas (not just inferred types) so consumers can derive validators, OpenAPI, AI tool definitions.
Fibers and shutdown
- Default to
Effect.fork(child dies with parent).Effect.forkScopedwhen the work outlives the originating effect but not the runtime.Effect.forkDaemonis a foot-gun: no supervision, must be interrupted manually. - Graceful shutdown: OS signal handlers interrupt the root fiber. Cascades through children automatically.
Tracing
Effect.withSpan(name, options)per unit of work.Effect.annotateCurrentSpanfor attributes. Spans nest via Effect context with no manual propagation.- For AI/agent code, mirror
@effect/ai's span attribute naming (gen_ai.system,gen_ai.request.model, tool name) so traces compose with theirs. @effect/opentelemetryis the canonical wiring.
Idiom and tree-shaking
import * as Effect from "effect/Effect"(namespace imports tree-shake; method-style breaks it).Effect.genfor control flow with branching and conditionals..pipefor linear data transformation. Switching from a long.andThenchain togenis almost always an improvement in readability.- Always pass
{ concurrency }toEffect.allandforEach. Unbounded parallelism is rarely what you want; useSemaphorefor rate limiting.
The anti-patterns
These show up over and over in production Effect code. If you spot one during an audit, flag it.
- Missing
yield*insideEffect.gen. Silent failure: you get anEffect<...>value back, not its result. Single most common bug. throwinstead ofEffect.failwith a tagged error.- Unbounded
Effect.all/Effect.forEachwith no concurrency limit. - Providing the same Layer in multiple places instead of once at entry.
Effect.runSyncwhererunFork/runPromiseis correct.runSynconly works when the effect is genuinely synchronous and has no async or scoped dependencies; otherwise it throws at runtime.- Methods over namespace functions (breaks tree-shaking).
- Long
.andThen/.flatMapchains that should beEffect.gen. accessors: trueon library services, leakingRinto every consumer.forkDaemonfor things that should beforkScoped, leaving orphaned fibers on shutdown.Data.TaggedErroracross a wire — useSchema.TaggedErrorfor anything serialized.- Decoding inside business logic instead of at the boundary; or worse, passing encoded types around.
Audit workflow
When the user asks for an audit, or you're reviewing Effect code:
- Identify the scope. Which files? The diff, or the whole package? Be explicit.
- Walk
references/audit-checklist.mdin order. Don't skip sections. - Collect findings with file:line references. Severity tags:
[bug](will misbehave),[idiom](works but not canonical),[smell](worth a second look). - Report before fixing. Show the findings, let the user choose what to apply. Resist the urge to rewrite half the codebase.
- When applying fixes, prefer the smallest change that resolves the finding. Don't bundle unrelated cleanups.
Sources of truth
When something is contested, prefer in order:
- The Effect source in
node_modules/effect/src(JSDoc and types are canonical) - The official docs at https://effect.website/docs/
- The
@effect/ai,@effect/platform,@effect/sqlsource as reference implementations - Effect Days talks and the team's public posts
- Community blogs (dtech.vision, EffectPatterns)
Community guidance (e.g. "avoid accessors: true on public APIs") is strong default-true but not in official docs. Say so when citing it; don't present it as canonical.
When you're not sure
Effect 4.x is in flight (Michael Arnaldi, Effect Days 2025). The Service/Layer model is stable; perf and codegen ergonomics are evolving. If you're about to recommend a deep refactor based on a pattern, check whether the user is on Effect 3.x or 4.x first (look at package.json), and skim the relevant section of node_modules/effect to confirm the API still matches what you remember.
If you genuinely don't know whether something is idiomatic, say so. The Effect community is small and opinionated; confident-sounding wrong advice is worse than "I'd check the source."