| name | effect-ts-expert |
| description | Expert guidance for Effect-TS architecture, typed error design, Layers, services, concurrency, resource management, and meaningful Effect refactoring. Use when the user explicitly asks for Effect patterns or the task materially changes Effect architecture. Do not use merely because a file imports Effect or a repository uses Effect. |
| source | https://github.com/ojowwalker77/Claude-Matrix/tree/main/skills/effect-ts |
Effect-TS Expert
Expert-level guidance for Effect-TS functional programming with typed errors, dependency injection, concurrency, and production-ready patterns.
Core Concepts
The Effect Type
Effect<Success, Error, Requirements>
Key insight: Effects are lazy descriptions of computations. They don't execute until run.
Creating Effects
import { Effect } from "effect"
const success = Effect.succeed(42)
const failure = Effect.fail(new Error("oops"))
const sync = Effect.sync(() => JSON.parse(data))
const trySync = Effect.try({
try: () => JSON.parse(data),
catch: (e) => new ParseError(e)
})
const promise = Effect.promise(() => fetch(url))
const tryPromise = Effect.tryPromise({
try: () => fetch(url).then(r => r.json()),
catch: (e) => new FetchError(e)
})
const callback = Effect.async<string, Error>((resume) => {
someCallbackApi((err, result) => {
if (err) resume(Effect.fail(err))
else resume(Effect.succeed(result))
})
})
Running Effects
Effect.runSync(effect)
Effect.runPromise(effect)
Effect.runPromiseExit(effect)
const runtime = ManagedRuntime.make(AppLayer)
await runtime.runPromise(effect)
Building Pipelines
pipe and Effect.gen
import { Effect, pipe } from "effect"
const program = pipe(
Effect.succeed(5),
Effect.map(n => n * 2),
Effect.flatMap(n => n > 5
? Effect.succeed(n)
: Effect.fail(new Error("too small"))
),
Effect.tap(n => Effect.log(`Result: ${n}`))
)
const program = Effect.gen(function* () {
const n = yield* Effect.succeed(5)
const doubled = n * 2
if (doubled <= 5) {
return yield* Effect.fail(new Error("too small"))
}
yield* Effect.log(`Result: ${doubled}`)
return doubled
})
Recommendation: Prefer Effect.gen for readability. Use pipe for simple transformations.
Error Handling
Typed Errors vs Defects
| Type | Use Case | Recovery |
|---|
| Typed Error | Domain failures (validation, not found, permissions) | Yes - caller can handle |
| Defect | Bugs, invariant violations, unrecoverable | No - terminates fiber |
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly id: string
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly message: string
}> {}
const findUser = (id: string): Effect.Effect<User, NotFoundError> =>
pipe(
db.query(id),
Effect.flatMap(user =>
user ? Effect.succeed(user) : Effect.fail(new NotFoundError({ id }))
)
)
const divide = (a: number, b: number): Effect.Effect<number> =>
b === 0
? Effect.die(new Error("Division by zero - this is a bug!"))
: Effect.succeed(a / b)
Error Recovery
Effect.catchAll(effect, (error) => Effect.succeed(fallback))
Effect.catchTag(effect, "NotFoundError", (e) =>
Effect.succeed(defaultUser)
)
Effect.catchTags(effect, {
NotFoundError: (e) => Effect.succeed(defaultUser),
ValidationError: (e) => Effect.fail(new HttpError(400, e.message))
})
Effect.either(effect)
Effect.retry(effect, Schedule.recurs(3))
Best Practice: Error Design
import { Schema } from "effect"
class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
status: Schema.Number,
message: Schema.String,
}) {}
Effect.fail(new Error("something went wrong"))
Effect.fail("error")
Dependency Injection
Services with Context.Tag
import { Context, Effect, Layer } from "effect"
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
readonly findById: (id: string) => Effect.Effect<User, NotFoundError>
readonly save: (user: User) => Effect.Effect<void>
}
>() {}
const getUser = (id: string) => Effect.gen(function* () {
const repo = yield* UserRepository
return yield* repo.findById(id)
})
const UserRepositoryLive = Layer.succeed(UserRepository, {
findById: (id) => Effect.tryPromise(() => db.users.find(id)),
save: (user) => Effect.tryPromise(() => db.users.save(user))
})
const program = getUser("123")
const runnable = Effect.provide(program, UserRepositoryLive)
Effect.Service (Simplified Pattern)
class Logger extends Effect.Service<Logger>()("Logger", {
sync: () => ({
log: (msg: string) => console.log(msg)
}),
effect: Effect.gen(function* () {
const config = yield* Config
return {
log: (msg: string) => Effect.sync(() =>
console.log(`[${config.level}] ${msg}`)
)
}
}),
dependencies: [ConfigLive]
}) {}
const program = Logger.log("Hello")
Effect.provide(program, Logger.Default)
Layer Composition
const BaseLayer = Layer.merge(ConfigLive, LoggerLive)
const DbLayer = Layer.provide(DatabaseLive, ConfigLive)
const AppLayer = pipe(
Layer.merge(ConfigLive, LoggerLive),
Layer.provideMerge(DatabaseLive),
Layer.provideMerge(UserRepositoryLive)
)
See references/layers.md for advanced patterns.
Concurrency
Fibers
const fiber = yield* Effect.fork(longRunningTask)
const result = yield* Fiber.join(fiber)
yield* Fiber.interrupt(fiber)
const fastest = yield* Effect.race(task1, task2)
const results = yield* Effect.all([task1, task2, task3])
const results = yield* Effect.all(tasks, { concurrency: 5 })
Synchronization Primitives
const counter = yield* Ref.make(0)
yield* Ref.update(counter, n => n + 1)
const value = yield* Ref.get(counter)
const queue = yield* Queue.bounded<number>(100)
yield* Queue.offer(queue, 42)
const item = yield* Queue.take(queue)
const sem = yield* Effect.makeSemaphore(3)
yield* sem.withPermits(1)(expensiveOperation)
const deferred = yield* Deferred.make<string, Error>()
yield* Deferred.succeed(deferred, "done")
const value = yield* Deferred.await(deferred)
Resource Management
Scoped Resources
const file = Effect.acquireRelease(
Effect.sync(() => fs.openSync(path, "r")),
(fd) => Effect.sync(() => fs.closeSync(fd))
)
const program = Effect.scoped(
Effect.gen(function* () {
const fd = yield* file
return yield* readFile(fd)
})
)
Finalizers
const program = Effect.gen(function* () {
yield* Effect.addFinalizer((exit) =>
Effect.log(`Cleanup: ${exit._tag}`)
)
})
const runnable = Effect.scoped(program)
Quick Reference
Common Operators
| Operator | Purpose |
|---|
Effect.map | Transform success value |
Effect.flatMap | Chain effects (monadic bind) |
Effect.tap | Side effect, keep original value |
Effect.andThen | Sequence, can be value or effect |
Effect.catchAll | Handle all errors |
Effect.catchTag | Handle specific tagged error |
Effect.provide | Inject dependencies |
Effect.retry | Retry with schedule |
Effect.timeout | Add timeout |
Effect.fork | Run concurrently |
Effect.all | Parallel execution |
When to Use What
| Scenario | Use |
|---|
| Transform value | Effect.map |
| Chain effects | Effect.flatMap or Effect.gen |
| Error recovery | Effect.catchTag / Effect.catchAll |
| Add logging | Effect.tap + Effect.log |
| Run in parallel | Effect.all with concurrency |
| Limit concurrency | Semaphore |
| Share mutable state | Ref |
| Producer/consumer | Queue |
| One-time signal | Deferred |
| Cleanup resources | Effect.acquireRelease |
Reference Documents
references/error-handling.md - Typed errors, defects, recovery patterns
references/layers.md - Dependency injection, service composition
references/concurrency.md - Fibers, synchronization, parallelism
references/streams.md - Stream, Sink, Channel patterns
references/schema.md - Validation, encoding/decoding
references/testing.md - Test layers, mocking, vitest integration
references/config.md - Configuration management
references/anti-patterns.md - Common mistakes and fixes
references/fp-ts-migration.md - Migration from fp-ts
Usage
This skill activates automatically when working with Effect-TS files or when the user mentions Effect, functional TypeScript, or typed errors.
Explicit invocation:
/effect-ts help me refactor this to use Effect
/effect-ts create a service with dependency injection
/effect-ts fix error handling in this code