Effect
Use this skill to write Effect code that reads like normal TypeScript where business logic matters, while still using Effect's composition model for errors, observability, retries, layers, and dependency wiring.
First Steps
- Inspect the project before editing:
- Check
package.jsonforeffect,@effect/*, TypeScript settings, test scripts, and lint/typecheck commands. - Search existing code for
Effect.gen,pipe,Layer,Context.Tag,Data.TaggedError, andcatchTag. - Match the repo's import style and Effect version. Do not introduce a second style casually.
- Check
- If the task is nontrivial, read Gen vs Pipe Patterns before changing code.
- Prefer a small typed Effect boundary over rewriting an entire module. Keep normal synchronous pure helpers as plain functions unless Effect adds real value.
Style Rule
Use Effect.gen for business logic and .pipe(...) for composition.
const program = Effect.gen(function* () {
// business flow, dependencies, branching, sequential steps
}).pipe(
// composition: tracing, retry, error recovery, layer provisioning
)
Decision Matrix
Use Effect.gen for:
- Injecting or retrieving dependencies with
yield* Service. - Sequential workflows where intermediate values matter.
- Conditional logic, branching, early returns, loops, and local variables.
- Business/domain logic that should be easy for a TypeScript developer to read.
- Interacting with the results of multiple Effect programs.
Use .pipe(...) for:
- Mapping or transforming the immediate result of one Effect.
- Error handling and recovery (
Effect.catchTag,Effect.catchAll,Effect.mapError). - Retry, timeout, schedule, tracing, logging, and spans.
- Layer building and dependency composition.
- Providing dependencies at the program edge.
Avoid:
- Long
Effect.andThenchains for domain workflows. - Hiding branches inside nested
flatMap/andThenwhenifinsideEffect.genis clearer. - Wrapping every pure helper in Effect. Pure functions are still useful.
- Throwing raw exceptions from business logic. Model expected failures as typed errors.
Implementation Workflow
1. Model Services
Use the service pattern already present in the repo. If there is no local convention, prefer Context.Tag plus a live Layer.
class Database extends Context.Tag("Database")<
Database,
{ readonly users: { create: (input: UserInput) => Effect.Effect<User, DatabaseError> } }
>() {}
Retrieve services inside Effect.gen:
const db = yield* Database
2. Model Expected Errors
Use tagged errors for domain and infrastructure failures. Keep error names stable and specific enough for catchTag.
class UserValidationError extends Data.TaggedError("UserValidationError")<{
readonly message: string
}> {}
When wrapping Promises, map unknown failures immediately:
Effect.tryPromise({
try: () => client.fetchUser(id),
catch: (cause) => new UserFetchError({ cause })
})
3. Write Business Logic With Gen
Keep the happy path and domain branches in one readable block.
const createUser = (input: UserInput) =>
Effect.gen(function* () {
const db = yield* Database
const validated = yield* validateUserInput(input)
const hashed = yield* hashPassword(validated.password)
const user = yield* db.users.create({ ...validated, password: hashed })
return yield* enrichUser(user)
})
4. Add Cross-Cutting Behavior With Pipe
Attach retry, spans, logging, and recovery outside the business flow.
const createUserProgram = createUser(input).pipe(
Effect.withSpan("create_user"),
Effect.retry(retryPolicy),
Effect.catchTag("UserAlreadyExistsError", () => Effect.succeed(null))
)
5. Compose Layers At The Edge
Keep dependency assembly separate from business logic.
const AppLayer = Layer.empty.pipe(
Layer.provide(DatabaseLive),
Layer.provide(LoggerLive),
Layer.provideMerge(CacheLive)
)
Refactoring Heuristics
- If a pipeline has 3+ sequential domain steps and named intermediate values would help, convert it to
Effect.gen. - If a
genblock ends with several operators that do not affect the domain path, move those operators to.pipe(...). - If code uses
try/catcharound async operations, convert the async boundary withEffect.tryPromiseand model the error. - If code mixes dependency construction with domain logic, extract a
Layeror service and inject it. - If onboarding is the concern, optimize for boring readability over point-free elegance.
Validation
After editing Effect code, run the smallest meaningful project checks:
pnpm test
pnpm typecheck
pnpm lint
Use the actual package manager/scripts from the repo. If no checks exist, run tsc --noEmit when available or at least inspect affected imports and inferred error/environment types.
References
- Gen vs Pipe Patterns: detailed thread-derived examples and refactoring patterns.