mcpField
E
effect-advanced

effect-advanced

github.com/trancong12102Development746 installsUpdated 5w ago
Repository

About effect-advanced

effect-advanced is an opinionated skill for building production Effect-TS applications. It sets conventions for typed errors, dependency injection, resource scopes, concurrency, and common pitfalls rather than documenting the full API.

Key features

  1. 01Effect.gen business logic conventions
  2. 02Typed error handling patterns
  3. 03Service and Layer architecture
  4. 04Scoped resource management
  5. 05High-level concurrency APIs
  6. 06Pitfall and reference guides

Use cases

  1. 01Structuring Effect-TS application services
  2. 02Handling typed failures and defects
  3. 03Composing dependency layers
  4. 04Managing resource lifetimes and concurrency

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/trancong12102/agentskills --skill effect-advanced

After installing, type/effect-advancedin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
concurrency.md
dependency-injection.md
error-handling.md
platform.md
resource-management.md
schema.md
stream.md
testing.md

Details

From the project README

Effect Advanced: Patterns, Conventions & Pitfalls

This skill defines the rules, conventions, and architectural decisions for building production Effect-TS applications. It is intentionally opinionated to prevent common pitfalls and enforce patterns that scale.

For detailed API documentation, use other appropriate tools (documentation lookup, web search, etc.) — this skill focuses on how and why to use Effect idiomatically, not the full API surface.

Core Conventions

Use Effect.gen for business logic

Generators read like synchronous code and are strongly preferred over long .pipe / .flatMap chains for anything beyond trivial composition:

const program = Effect.gen(function* () {
  const config = yield* ConfigService;
  const user = yield* UserRepo.findById(config.userId);
  return user;
});

Reserve pipe for data transformation pipelines and short combinator chains.

Never throw — use Effect's error channel
Instead of...Use
throw new Error()Effect.fail(new MyError())
try/catch on promisesEffect.tryPromise({ try, catch })
Callback APIsEffect.async((resume) => ...)
Unrecoverable crashesEffect.die(defect)
Functions over methods

Prefer Effect.map(e, f) over e.pipe(Effect.map(f)) for composability and tree-shaking. Flat imports (import { Effect } from "effect") are fine for applications; namespace imports (import * as Effect from "effect/Effect") are better for libraries.

@effect/schema is deprecated

Schema has been merged into core effect. Import from "effect" directly:

import { Schema } from "effect";
// NOT: import { Schema } from "@effect/schema"
Use NodeRuntime.runMain in production

Effect.runPromise does not handle SIGINT/SIGTERM gracefully:

import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));

Error Handling Philosophy

Failures vs defects — the fundamental distinction
AspectFailure (expected)Defect (unexpected)
APIEffect.fail(new MyError())Effect.die(new Error())
Type channelTracked in ENever appears in E (never)
RecoverycatchTag, catchAll, retryOnly at system boundaries
Rule of thumbYou intend to handle it at call siteBug or impossible state
Always use tagged errors

Plain Error or string failures miss the value of Effect's typed error channel:

class UserNotFound extends Data.TaggedError("UserNotFound")<{
  readonly id: string;
}> {}

// Tagged errors are yieldable — no Effect.fail wrapper needed
const program = Effect.gen(function* () {
  const user = yield* db.findUser(id);
  if (!user) yield* new UserNotFound({ id });
  return user;
});
catchAll does NOT catch defects

This is the #1 error handling mistake:

Effect.catchAll(program, handler); // catches E only — NOT defects
Effect.catchAllCause(program, handler); // catches everything (E + defects + interrupts)

Only use catchAllCause / catchAllDefect at system boundaries (top-level error handlers, HTTP response mappers).


Dependency Injection Architecture

Service → Layer → Provide (once)
1. Define services with Context.Tag  →  "what do I need?"
2. Implement via Layers              →  "how is it built?"
3. Provide once at entry point       →  "wire it all together"
Service methods must have R = never

Dependencies belong in Layer composition, not method signatures:

// WRONG: leaks dependency to callers
findById: (id: string) => Effect.Effect<User, UserNotFound, Database>;

// RIGHT: Database is wired in the Layer
findById: (id: string) => Effect.Effect<User, UserNotFound>;
Layer composition — know the operators
OperationWhenBehavior
Layer.merge(A, B)Independent servicesBoth build concurrently
Layer.provide(downstream, upstream)A feeds Bupstream builds first
Layer.fresh(layer)Force new instanceBypasses memoization

Critical: Layer.merge does NOT sequence construction. If B depends on A, use Layer.provide, not Layer.merge.

One Effect.provide at the entry point

Scattered provide calls create hidden dependencies and layer duplication:

// WRONG: provide scattered throughout codebase
const getUser = UserRepo.findById(id).pipe(Effect.provide(DbLayer));

// RIGHT: compose and provide once
const main = program.pipe(Effect.provide(AppLayer));
NodeRuntime.runMain(main);

Resource & Scope Rules

Effect.scoped is mandatory for acquireRelease

Forgetting Effect.scoped is the #1 resource management pitfall — resources accumulate until the program exits:

// WRONG: scope never closes, connection leaks
const result = yield * getDbConnection;

// RIGHT: scope closes when block completes
const result =
  yield *
  Effect.scoped(
    Effect.gen(function* () {
      const conn = yield* getDbConnection;
      return yield* conn.query("SELECT 1");
    }),
  );
Release finalizers always run

On success, failure, AND interruption — guaranteed. The finalizer receives the Exit value for conditional cleanup.

Multiple resources in one scope
Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* Effect.acquireRelease(openConn(), closeConn);
    const file = yield* Effect.acquireRelease(openFile(), closeFile);
    // both released when scope closes, in REVERSE acquisition order
  }),
);

Concurrency Model

Prefer high-level APIs over raw fork
APIUse case
Effect.all([], { concurrency: N })Bounded parallel execution
Effect.forEach(items, fn, { concurrency: N })Worker pool pattern
Effect.race(a, b)First to complete wins, others interrupted
Effect.timeout(e, dur)Deadline on any effect

Only reach for Effect.fork / Fiber when high-level APIs are insufficient.

Fork variants — know the lifecycle
FunctionScopeCleanup
Effect.forkParent's scopeAuto-interrupted with parent
Effect.forkDaemonGlobal scopeNothing cleans it up — you must
Effect.forkScopedNearest ScopeTied to resource lifecycle

Gotcha: forkDaemon leaks fibers if you forget to interrupt them.


Common Pitfalls

  1. Floating effects — creating an Effect without yielding or running it is a silent bug. Effect.log("msg") inside a generator does nothing unless yield*-ed.

  2. catchAll won't catch defects — use catchAllCause at system boundaries for full failure visibility.

  3. Missing Effect.scoped — acquireRelease without a scope boundary leaks resources until program exit.

  4. Scattered Effect.provide — compose all layers and provide once at the entry point.

  5. Point-free on overloaded functions — Effect.map(myOverloadedFn) silently erases generics. Use explicit lambdas: Effect.map((x) => myOverloadedFn(x)).

  6. Effect.async resume called multiple times — resume must be called exactly once. Multiple calls cause undefined behavior.

  7. orDie silences errors — converts typed failures to untyped defects. Handle errors properly instead.

  8. Layer.merge for dependent services — merge doesn't sequence construction. Use Layer.provide when one layer needs another's output.

  9. Fiber.join vs Fiber.await — join can cause premature finalizer execution in edge cases. Prefer await when resource safety matters.

  10. runCollect on infinite streams — never call without a prior take. It will never terminate and consume unbounded memory.

  11. Using it.effect for scoped tests — effects requiring Scope must use it.scoped, not it.effect, or you get a type error.


Reference Files

Read the relevant reference file when working with a specific concern:

FileWhen to read
references/error-handling.mdTagged errors, Cause, defect recovery, error mapping patterns
references/dependency-injection.mdServices, Layers, composition, memoization, provide patterns
references/concurrency.mdFibers, fork variants, Deferred, Semaphore, structured concurrency
references/resource-management.mdScope, acquireRelease, Layer resources, fork + scope interaction
references/schema.mdSchema definition, transforms, branded types, recursive schemas
references/stream.mdStream operators, chunking, backpressure, resourceful streams
references/testing.md@effect/vitest, TestClock, Layer mocking, Config mocking
references/platform.mdHTTP client, FileSystem, Command, runtime, framework integration

Comments

Markdown supported