Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library.
Forbidden patterns, Effect.fn vs Effect.fnUntraced, resource management
references/testing-patterns.md
@effect/vitest with assert, TestClock, service mocking
references/quality-tooling-and-resources.md
Anti-patterns, validation checklist, packages
Core Principles
Effect is not just for async — Use Effect for any fallible operation
Immutability — Use Effect's immutable data structures (Data, Chunk, HashSet)
Type Safety — Track errors in types. No any or unknown in error channels
Composition — Build programs by composing small Effects
Schema-First — Define data models using Schema with branded types
Pattern Matching — Use Match for all branching over tagged unions (never switch/if-else on _tag)
Effect Data Types — Use Option (not null), Either (not ad-hoc), Duration (not raw ms), DateTime (not Date), BigDecimal (not floats), Redacted (for secrets). See references/data-types.md
Quick Reference
Import Convention
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Match from "effect/Match";
import * as Option from "effect/Option";
import * as Either from "effect/Either";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import { pipe } from "effect/Function";
// Also: DateTime, BigDecimal, Chunk, HashSet, Exit, Cause, Redacted
Always use Match — Match.exhaustive catches missing cases at compile time. See references/pattern-matching.md.
// Match.type — reusable matcher function
const handle = Match.type<Status>().pipe(
Match.tag("Pending", (s) => `Pending since ${s.requestedAt}`),
Match.tag("Approved", (s) => `Approved by ${s.approvedBy}`),
Match.exhaustive // Compile error if any variant is missing
);
// Match.valueTags — shorthand for immediate matching
Match.valueTags(status, {
Pending: (s) => `Pending since ${s.requestedAt}`,
Approved: (s) => `Approved by ${s.approvedBy}`,
});
Service Pattern (Context.Tag)
See references/class-patterns.md for full pattern with factory methods and layers.
CRITICAL: Use assert from @effect/vitest for it.effect. Never expect with it.effect. See references/testing-patterns.md.
import { assert, describe, it } from "@effect/vitest";
it.effect("processes data", () =>
Effect.gen(function* () {
const result = yield* processData("input");
assert.strictEqual(result, "expected");
}).pipe(Effect.provide(MyService.testLayer))
);
Forbidden Patterns
// NEVER: try-catch in Effect.gen — use Effect.exit instead
Effect.gen(function* () {
try { yield* someEffect } catch (e) { } // WRONG — will never catch
});
// NEVER: Type assertions
const value = something as any; // FORBIDDEN
const value = something as never; // FORBIDDEN
// NEVER: Missing return on terminal yield
Effect.gen(function* () {
if (bad) { yield* Effect.fail("err") } // Missing return!
});
// NEVER: switch/if-else on _tag — use Match instead
switch (status._tag) { /* no exhaustiveness checking! */ }
// NEVER: Effect.runSync inside Effects
Effect.gen(function* () { Effect.runSync(sideEffect) }); // Loses error tracking
// NEVER: Native JS where Effect data types exist
const x: string | null = null; // Use Option<string>
const delay = 5000; // Use Duration.seconds(5)
const now = new Date(); // Use DateTime.now or DateTime.unsafeNow()
const price = 0.1 + 0.2; // Use BigDecimal for precision
const secret = "sk-1234"; // Use Redacted.make("sk-1234")
// NEVER: expect with it.effect
it.effect("test", () => Effect.gen(function* () {
expect(result).toBe(value) // WRONG — use assert.strictEqual
}));
// NEVER: Inline layers (breaks memoization)
Layer.provide(Postgres.layer({ url })) // Store in constant instead
Validation Checklist
Imports use import * as Module from "effect/Module"
Effect.gen for complex logic, pipe for linear, Effect.fn for public API
Match for all _tag branching with Match.exhaustive
Branded types for domain primitives (IDs, Emails)
Errors: Data.TaggedError (discrimination) or Schema.TaggedError (serializable)
No any/unknown in error channels, no type assertions
No try-catch in Effect.gen — use Effect.exit
return yield* for terminal effects (Effect.fail, Effect.interrupt)
Services: Context.Tag with static factory methods and Effect.fn tracing
Layers: Layer.merge/Layer.provide, parameterized layers in constants
Resources: Effect.acquireRelease or Effect.scoped
Option for nullable values, Either for sync success/failure
Duration for time values, DateTime for dates (not Date)
BigDecimal for financial/precise math, Redacted for secrets
Data.struct/Data.Class for structural equality, HashSet for sets
Clock.currentTimeMillis instead of Date.now()
Tests: assert from @effect/vitest (not expect) with it.effect
Run pnpm run typecheck and pnpm run test
Reference Implementation
See /packages/looper/src/data/api-client/api-client.ts for Context.Tag service pattern.
Effect Solutions CLI
pnpm exec effect-solutions list # List all topics
pnpm exec effect-solutions show <slug...> # Read topics
pnpm exec effect-solutions search <term> # Search by keyword
1---2name: effect-ts3description: Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library.4---56# Effect-TS Developer Guide78Guidelines, patterns, and best practices for Effect-TS in this project.910## Reference Documents1112Read the relevant reference before writing code. `references/core-patterns.md` is the master index.1314| Reference | Topics |15|---|---|16| `references/foundations.md` | Setup, imports, TypeScript config |17| `references/construction-and-style.md` | `Effect.gen`, `pipe`, `Effect.fn`, `Effect.fnUntraced` |18| `references/schema-errors-config.md` | Schema modeling, errors, config, retry |19| `references/pattern-matching.md` | **`Match.type`, `Match.value`, `Match.tag`, `Match.exhaustive`** — mandatory for tagged unions |20| `references/control-flow-and-runtime.md` | `Effect.if`, `Effect.when`, loops, `runSync`, `runPromise`, `ManagedRuntime` |21| `references/data-types.md` | **All data types**: `Option`, `Either`, `Data`, `Exit`, `Cause`, `Duration`, `DateTime`, `BigDecimal`, `Chunk`, `HashSet`, `Redacted` |22| `references/data-and-testing.md` | Option/Either/Array quick ref, `@effect/vitest` setup |23| `references/concurrency-and-resources.md` | Concurrency, `Scope`, finalizers, resources |24| `references/streams-deep-dive.md` | Creating, operations, grouping, partitioning, broadcasting, buffering, throttling, error handling |25| `references/sink.md` | Sink constructors, collecting, folding, operations, concurrency, leftovers, `Stream.transduce` |26| `references/batching-and-caching.md` | Request batching (`RequestResolver`), `cachedWithTTL` |27| `references/schema-transforms-and-filters.md` | `Schema.transform`, `Schema.filter`, refinements |28| `references/api-platform-observability.md` | `HttpApi`, logging, tracing, spans |29| `references/class-patterns.md` | `Context.Tag` service pattern, layers, memoization, testing |30| `references/error-handling-patterns.md` | `Data.TaggedError`, `Schema.TaggedError`, error composition, recovery |31| `references/library-development-patterns.md` | Forbidden patterns, `Effect.fn` vs `Effect.fnUntraced`, resource management |32| `references/testing-patterns.md` | `@effect/vitest` with `assert`, `TestClock`, service mocking |33| `references/quality-tooling-and-resources.md` | Anti-patterns, validation checklist, packages |3435## Core Principles36371. **Effect is not just for async** — Use `Effect` for any fallible operation382. **Immutability** — Use Effect's immutable data structures (`Data`, `Chunk`, `HashSet`)393. **Type Safety** — Track errors in types. No `any` or `unknown` in error channels404. **Composition** — Build programs by composing small Effects415. **Schema-First** — Define data models using `Schema` with branded types426. **Pattern Matching** — Use `Match` for all branching over tagged unions (never `switch`/`if-else` on `_tag`)437. **Effect Data Types** — Use `Option` (not null), `Either` (not ad-hoc), `Duration` (not raw ms), `DateTime` (not Date), `BigDecimal` (not floats), `Redacted` (for secrets). See `references/data-types.md`4445## Quick Reference4647### Import Convention4849```typescript50import * as Context from "effect/Context";51import * as Effect from "effect/Effect";52import * as Layer from "effect/Layer";53import * as Schema from "effect/Schema";54import * as Match from "effect/Match";55import * as Option from "effect/Option";56import * as Either from "effect/Either";57import * as Data from "effect/Data";58import * as Duration from "effect/Duration";59import { pipe } from "effect/Function";60// Also: DateTime, BigDecimal, Chunk, HashSet, Exit, Cause, Redacted61```6263### Effect.gen vs pipe vs Effect.fn6465```typescript66// Effect.gen — complex logic with branching67Effect.gen(function* () {68 const user = yield* fetchUser(id);69 if (user.isAdmin) yield* logAdminAccess(user);70 return user;71});7273// pipe — linear transformations74pipe(fetchData(), Effect.map(transform), Effect.flatMap(save));7576// Effect.fn — traced reusable functions (public API)77const processUser = Effect.fn("processUser")(function* (userId: string) {78 const user = yield* getUser(userId);79 return yield* processData(user);80});81```8283### Error Handling8485`Data.TaggedError` for in-process discrimination, `Schema.TaggedError` for serializable errors. See `references/error-handling-patterns.md`.8687```typescript88export class NotFoundError extends Data.TaggedError("NotFoundError")<{89 id: string;90}> {}9192// Recovery93pipe(riskyOp, Effect.catchTag("NotFoundError", (e) => Effect.succeed(null)));94```9596### Pattern Matching (Mandatory for Tagged Unions)9798**Always use `Match`** — `Match.exhaustive` catches missing cases at compile time. See `references/pattern-matching.md`.99100```typescript101// Match.type — reusable matcher function102const handle = Match.type<Status>().pipe(103 Match.tag("Pending", (s) => `Pending since ${s.requestedAt}`),104 Match.tag("Approved", (s) => `Approved by ${s.approvedBy}`),105 Match.exhaustive // Compile error if any variant is missing106);107108// Match.valueTags — shorthand for immediate matching109Match.valueTags(status, {110 Pending: (s) => `Pending since ${s.requestedAt}`,111 Approved: (s) => `Approved by ${s.approvedBy}`,112});113```114115### Service Pattern (Context.Tag)116117See `references/class-patterns.md` for full pattern with factory methods and layers.118119```typescript120export class MyService extends Context.Tag("@myapp/MyService")<121 MyService,122 { readonly find: (id: string) => Effect.Effect<Result, NotFoundError> }123>() {124 static readonly layer = Layer.effect(MyService, Effect.gen(function* () {125 const db = yield* Database;126 return MyService.of({ find: MyService.createFind(db) });127 }));128}129```130131### Testing132133**CRITICAL**: Use `assert` from `@effect/vitest` for `it.effect`. Never `expect` with `it.effect`. See `references/testing-patterns.md`.134135```typescript136import { assert, describe, it } from "@effect/vitest";137138it.effect("processes data", () =>139 Effect.gen(function* () {140 const result = yield* processData("input");141 assert.strictEqual(result, "expected");142 }).pipe(Effect.provide(MyService.testLayer))143);144```145146## Forbidden Patterns147148```typescript149// NEVER: try-catch in Effect.gen — use Effect.exit instead150Effect.gen(function* () {151 try { yield* someEffect } catch (e) { } // WRONG — will never catch152});153154// NEVER: Type assertions155const value = something as any; // FORBIDDEN156const value = something as never; // FORBIDDEN157158// NEVER: Missing return on terminal yield159Effect.gen(function* () {160 if (bad) { yield* Effect.fail("err") } // Missing return!161});162163// NEVER: switch/if-else on _tag — use Match instead164switch (status._tag) { /* no exhaustiveness checking! */ }165166// NEVER: Effect.runSync inside Effects167Effect.gen(function* () { Effect.runSync(sideEffect) }); // Loses error tracking168169// NEVER: Native JS where Effect data types exist170const x: string | null = null; // Use Option<string>171const delay = 5000; // Use Duration.seconds(5)172const now = new Date(); // Use DateTime.now or DateTime.unsafeNow()173const price = 0.1 + 0.2; // Use BigDecimal for precision174const secret = "sk-1234"; // Use Redacted.make("sk-1234")175176// NEVER: expect with it.effect177it.effect("test", () => Effect.gen(function* () {178 expect(result).toBe(value) // WRONG — use assert.strictEqual179}));180181// NEVER: Inline layers (breaks memoization)182Layer.provide(Postgres.layer({ url })) // Store in constant instead183```184185## Validation Checklist186187- [ ] Imports use `import * as Module from "effect/Module"`188- [ ] `Effect.gen` for complex logic, `pipe` for linear, `Effect.fn` for public API189- [ ] `Match` for all `_tag` branching with `Match.exhaustive`190- [ ] Branded types for domain primitives (IDs, Emails)191- [ ] Errors: `Data.TaggedError` (discrimination) or `Schema.TaggedError` (serializable)192- [ ] No `any`/`unknown` in error channels, no type assertions193- [ ] No try-catch in `Effect.gen` — use `Effect.exit`194- [ ] `return yield*` for terminal effects (`Effect.fail`, `Effect.interrupt`)195- [ ] Services: `Context.Tag` with static factory methods and `Effect.fn` tracing196- [ ] Layers: `Layer.merge`/`Layer.provide`, parameterized layers in constants197- [ ] Resources: `Effect.acquireRelease` or `Effect.scoped`198- [ ] `Option` for nullable values, `Either` for sync success/failure199- [ ] `Duration` for time values, `DateTime` for dates (not `Date`)200- [ ] `BigDecimal` for financial/precise math, `Redacted` for secrets201- [ ] `Data.struct`/`Data.Class` for structural equality, `HashSet` for sets202- [ ] `Clock.currentTimeMillis` instead of `Date.now()`203- [ ] Tests: `assert` from `@effect/vitest` (not `expect`) with `it.effect`204- [ ] Run `pnpm run typecheck` and `pnpm run test`205206## Reference Implementation207208See `/packages/looper/src/data/api-client/api-client.ts` for Context.Tag service pattern.209210## Effect Solutions CLI211212```bash213pnpm exec effect-solutions list # List all topics214pnpm exec effect-solutions show <slug...> # Read topics215pnpm exec effect-solutions search <term> # Search by keyword216```
Run npx skillmds@latest add pedronauck/effect-ts in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Expert guide for writing Effect-TS code, including project setup, core principles, data modeling with Schema, error handling, and the Context.Tag service pattern. Use when writing, refactoring, or analyzing TypeScript code using the Effect library. It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
pedronauck (@pedronauck) published this skill. Their other Agent Skills are listed on their SkillMD profile.