Effect v4
Use this skill for general Effect mechanics and routing. For Theseus product/domain choices, use the Theseus design skill.
Route To Narrow Skills
Load the narrower skill when the task clearly matches it:
effect-services-layers- Context.Service, service interfaces, Layer construction, dependency graphs, test seams.effect-errors-schema- typed failures, defects, Cause, Schema decoding/encoding, schema-backed errors, boundary normalization.effect-concurrency-lifecycle- Fiber, Deferred, Queue, PubSub, Stream, Scope, interruption, background loops, backpressure.effect-observability-time- Clock, Duration, timeout, retry schedules, logging, tracing spans, metrics, config, redaction.effect-testing-runtime- Effect.runPromise boundaries, test layers, deterministic services, TestClock/TestContext, runtime verification.
If several apply, load only the ones needed for the change.
Operating Model
Effect code is explicit about:
- what succeeds
- what can fail
- what services are required
- what resources must be acquired and released
- what runs concurrently and how it is interrupted
Good Effect code keeps these facts in the type signature until a deliberate boundary handles them.
Workflow
Effect APIs move. Before using an unfamiliar API:
- Search current repo usage.
- Resolve the installed
effectpackage from the lockfile or package manager layout, then inspect its local type declarations. - Prefer patterns already used in
packages/theseus-coreandpackages/theseus-server. - Treat web examples as v3 unless verified against local v4 types.
Useful searches:
rg "Context.Service|Layer.mergeAll|Layer.provideMerge|Effect.catchTag|Effect.forkDetach" packages
find node_modules -path '*effect*/dist/Effect.d.ts' -o -path '*effect*/dist/Schema.d.ts'
rg "export declare const catch|forkDetach|TaggedErrorClass" node_modules
The repo already has @effect/language-service configured in root tsconfig.json. Trust its diagnostics when Effect types look strange.
Effect language-service commands:
bun run effect:diagnostics- run Effect diagnostics across configured non-web packages.bun run effect:ls:check- check whether local TypeScript is patched for build-time Effect diagnostics.bun run effect:ls:patch- patch local TypeScript so Effect diagnostics surface through compiler tooling.
Type Shape
Effect.Effect<Success, Error, Requirements> means:
Success- success valueError- expected typed failureRequirements- required services/environment
Expected failures belong in the error channel. Defects are bugs, thrown exceptions, rejected promise defects, or violated invariants; handle them only at boundaries.
Core Constructors
Use the constructor that matches the boundary:
Effect.succeed(value) // pure success value
Effect.fail(error) // expected typed failure
Effect.sync(() => value) // sync, non-throwing side effect
Effect.try(() => risky()) // sync code that may throw
Effect.promise(() => p) // promise that cannot usefully map rejection
Effect.tryPromise({ // promise with rejection normalized to typed failure
try: () => fetchThing(),
catch: (cause) => new FetchFailed({ cause }),
})
Rules:
- Use
Effect.try/Effect.tryPromiseat foreign boundaries. - Normalize foreign exceptions into typed errors as early as possible.
- Do not wrap already-effectful code in
tryPromise. - Do not use
Effect.runPromiseas a composition tool.
Composition
Prefer Effect.gen for sequential business logic and pipe combinators for local transformations.
const program = Effect.gen(function* () {
const user = yield* Users.find(userId)
const account = yield* Accounts.find(user.accountId)
yield* Audit.log({ type: "account.viewed", userId })
return account
})
Rules:
- Use
Effect.mapfor success-value transformation. - Use
Effect.flatMapwhen the next step returns an Effect. - Use
Effect.tapfor effectful observation without changing the value. - Use
Effect.allfor independent effects; setconcurrencywhen work is unbounded or expensive. - Use
Effect.partitionwhen partial success is a valid outcome. - Use validation mode on collection operations only when the caller needs all failures, not fail-fast behavior.
- Keep
Effect.genblocks linear; extract named effects when nesting grows. - Use
Effect.fn("Name")for important service methods or runtime operations where tracing and better diagnostic names matter.
Pattern Matching
Use Match for tagged unions or multi-branch domain logic when chained conditionals obscure exhaustiveness.
import { Match } from "effect"
const render = Match.type<Event>().pipe(
Match.tag("Started", (event) => `started ${event.id}`),
Match.tag("Done", (event) => `done ${event.id}`),
Match.exhaustive,
)
Rules:
- Prefer
Match.tag/Match.tagsfor_tagunions. - Prefer exhaustive matching when the input union is closed.
- Keep simple two-branch cases as normal conditionals when that is clearer.
v4 Translation Table
Common v3 or stale examples need translation:
| Stale pattern | Theseus v4 pattern |
|---|---|
Effect.catchAll |
Effect.catch |
Effect.catchAllDefect |
Effect.catchDefect |
Context.Tag for services |
Context.Service<Service, Shape>()("Name") |
uncurried Layer.effect(Tag, effect) |
Layer.effect(Tag)(effect) |
ServiceMap.Service |
removed; use Context.Service |
Schema.TaggedError |
Schema.TaggedErrorClass for schema-backed failures |
| bare millisecond numbers for time | Duration.millis, Duration.seconds, or accepted duration strings when verified |
throw in Effect.gen |
return yield* Effect.fail(error) or return yield* error for yieldable errors |
Do not cargo-cult Effect.Service unless local v4 types and repo patterns support it for the case at hand. This repo currently uses Context.Service.
Anti-Patterns
Avoid these unless the file is explicitly a process/test/script boundary:
Effect.runPromiseorEffect.runSyncinside services or domain functions.throwfor expected failures insideEffect.gen.try/catcharoundyield*expecting to catch typed Effect failures.console.login runtime code.- direct
process.envaccess outside config boundaries. Option.getOrThrowin runtime code.- unbounded queues or concurrency without an explicit bounding argument.
- type assertions that erase the error or requirements channel (
as any,as never) instead of fixing the layer/error model.
Theseus Checks
Before finalizing Effect changes:
- Search for matching local patterns.
- Re-read the involved service/layer/error signatures.
- Check whether the code is at a boundary or inside the domain; choose schemas/errors accordingly.
- Run the narrow package test when behavior changed.
- Run
bun run typecheckafter service, layer, schema, or error-channel changes. - If an API was inferred from external material, verify it in the locally installed Effect type declarations before committing the pattern.