Claude Code for Effect-TS — Workflow Guide
The Setup
You are building a TypeScript application using Effect-TS, a library for typed functional effects that handles errors, dependencies, and async operations in a composable way. Claude Code can write Effect programs and services, but it defaults to try/catch patterns and misunderstands Effect’s pipe-based composition and dependency injection system.
What Claude Code Gets Wrong By Default
-
Uses try/catch for error handling. Claude wraps operations in try/catch blocks. Effect-TS uses typed errors in the
Effect<Success, Error, Requirements>type signature and handles them withEffect.catchTag()orEffect.catchAll(). -
Creates classes with constructor injection. Claude writes DI using constructor parameters or InversifyJS. Effect-TS uses
Context.TagandLayerfor dependency injection — services are defined as tags and provided through layer composition. -
Writes Promise-based async code. Claude uses
async/awaitwith Promises. Effect-TS wraps async operations inEffect.tryPromise()and chains them withpipe()andEffect.flatMap(). -
Ignores the generator syntax. Claude writes deeply nested
pipe()chains. Effect-TS supportsEffect.gen(function* () { ... })syntax that looks like async/await but preserves full type safety for errors and requirements.
The CLAUDE.md Configuration
# Effect-TS Project
## Architecture
- Core: Effect-TS (effect package)
- Pattern: Typed effects with Service/Layer architecture
- Error handling: Typed errors, no try/catch
- DI: Context.Tag + Layer composition
## Effect-TS Rules
- Use Effect.gen(function* () { }) for readable effect chains
- Errors are typed: Effect<A, E, R> where E is the error channel
- Services defined with Context.Tag: class MyService extends Context.Tag("MyService")
- Dependencies provided via Layer: Layer.succeed, Layer.effect
- Use Effect.tryPromise for wrapping async operations
- Schema validation with @effect/schema, not Zod
- Never throw errors — use Effect.fail() or Effect.die()
- Run effects with Effect.runPromise() at the edge (entry points only)
## Conventions
- Services in src/services/ directory
- Layers composed in src/layers/ or src/main.ts
- Errors defined as tagged unions: class NotFound extends Data.TaggedError("NotFound")
- Use pipe() for composition: pipe(effect, Effect.map(...), Effect.flatMap(...))
- Prefer Effect.gen over deep pipe chains for readability
- Runtime created once in src/runtime.ts
Workflow Example
You want to create a user service with typed errors. Prompt Claude Code:
“Create an Effect-TS user service with getById and create methods. The getById should fail with a typed NotFoundError. The create method should validate input with @effect/schema and fail with a ValidationError. Provide the service as a Layer.”
Claude Code should define NotFoundError and ValidationError as Data.TaggedError classes, create a UserService tag with Context.Tag, implement it using Effect.gen, define the layer with Layer.succeed(UserService, implementation), and wire error handling using Effect.catchTag("NotFound", ...).
Common Pitfalls
-
Mixing Effect and Promise in the same function. Claude awaits an Effect inside an async function. Effects must be run with
Effect.runPromise()only at the program boundary. Inside Effect code, useEffect.flatMapto chain, neverawait. -
Forgetting to provide required layers. Claude creates effects that require services but runs them without providing layers. The TypeScript compiler catches this as the
R(requirements) channel is notnever, but Claude ignores the type error and usesas any. -
Using
Effect.syncfor async operations. Claude wraps fetch calls inEffect.sync(() => fetch(...)). Sync effects cannot contain async code — useEffect.tryPromise()for anything that returns a Promise, orEffect.asyncfor callback-based APIs.