effective-effect

Enforces Effect-TS patterns for services, errors, layers, and atoms. Use when writing code with Context.Tag, Schema.TaggedError, Layer composition, or effect-atom React components.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "effective-effect" with this command: npx skills add stromseng/skills/stromseng-skills-effective-effect

Effect-TS Best Practices

This skill enforces opinionated, consistent patterns for Effect-TS codebases. These patterns optimize for type safety, testability, observability, and maintainability.

Core Principles

Effect Type Signature

Effect<Success, Error, Requirements>
//      ↑        ↑       ↑
//      |        |       └── Dependencies (provided via Layers)
//      |        └── Expected errors (typed, must be handled)
//      └── Success value

Prefer Explicit Over Generic Errors

Every distinct failure reason deserves its own error type. Don't collapse multiple failure modes into generic HTTP errors like NotFoundError or BadRequestError.

// BAD - Generic errors lose context
class NotFoundError extends Schema.TaggedError<NotFoundError>()("NotFoundError", {
  message: Schema.String, // Dont use `message` as it may hide context when using Effect.log
}) {}

// GOOD - Specific errors enable precise handling
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
  userId: UserId,
  userMessage: Schema.String,
}) {}

class SessionExpiredError extends Schema.TaggedError<SessionExpiredError>()("SessionExpiredError", {
  expiredAt: Schema.Date,
  userMessage: Schema.String,
}) {}

// GOOD - Wrapping errors with cause preserves stack traces
class UserLookupError extends Schema.TaggedError<UserLookupError>()("UserLookupError", {
  userId: UserId,
  reason: Schema.String, // prefer using `reason` over `message` for logging
  cause: Schema.Defect, // Wraps the underlying error - Effect.log prints as stack trace
}) {}

Benefits:

  • UserNotFoundError with userId → Frontend shows "User doesn't exist"
  • SessionExpiredError with expiredAt → Frontend shows "Session expired, please log in"
  • Type-safe error handling with catchTag/catchTags
  • Not using the message field allows Effect.log to log all error context values. This way we will see both the expiredAt and userMessage fields in the logs, if we log the error.
  • Using a cause field of type Schema.Defect preserves the original stack trace when logging the error.

Quick Reference: Critical Rules

CategoryDODON'T
Services (app)Effect.Service with default implementationInline dependencies in methods
Services (lib)Context.Tag when no sensible default existsAssuming implementation in library code
Dependenciesdependencies: [...] or yield in LayerPass services as function parameters
ErrorsSchema.TaggedError (yieldable)Plain classes or generic Error
Error RecoverycatchTag/catchTags with pattern matchingcatchAll losing type info
IDsSchema.String.pipe(Schema.brand("UserId"))Plain string for entity IDs
FunctionsEffect.fn("Service.method")Anonymous generators
SequencingEffect.gen with yield*Nested .then() or .pipe() chains
LoggingEffect.log with structured dataconsole.log
ConfigSchema.Config or Config.* primitivesprocess.env directly
OptionsOption.match with both casesOption.getOrThrow
NullabilityOption<T> in domain typesnull/undefined
Test LayersLayer.sync with in-memory stateMocking frameworks
AtomsAtom.make outside componentsCreating atoms inside render
Atom ResultsResult.builder with onErrorTagIgnoring loading/error states

Basics

Effect.gen

Just as async/await provides a sequential, readable way to work with Promise values, Effect.gen and yield* provide the same ergonomic benefits for Effect values:

import { Effect } from "effect";

const program = Effect.gen(function* () {
  const data = yield* fetchData;
  yield* Effect.logInfo(`Processing data: ${data}`);
  return yield* processData(data);
});

Effect.fn

Use Effect.fn with generator functions for traced, named effects. Effect.fn traces where the function is called from, not just where it's defined:

import { Effect } from "effect";

const processUser = Effect.fn("processUser")(function* (userId: string) {
  yield* Effect.logInfo(`Processing user ${userId}`);
  const user = yield* getUser(userId);
  return yield* processData(user);
});

Benefits:

  • Call-site tracing for each invocation
  • Stack traces with location details
  • Clean signatures
  • Automatic spans for telemetry

Pipe for Instrumentation

Use .pipe() to add cross-cutting concerns to Effect values:

import { Effect, Schedule } from "effect";

const program = fetchData.pipe(
  Effect.timeout("5 seconds"),
  Effect.retry(Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(3)))),
  Effect.tap((data) => Effect.logInfo(`Fetched: ${data}`)),
  Effect.withSpan("fetchData"),
);

Common instrumentation:

  • Effect.timeout - fail if effect takes too long
  • Effect.retry - retry on failure with a schedule
  • Effect.tap - run side effect without changing the value
  • Effect.withSpan - add tracing span

Service Definition Pattern

Effect provides two ways to model services: Effect.Service and Context.Tag. Choose based on your use case:

FeatureEffect.ServiceContext.Tag
Best forApplication code with clear implementationLibrary code or dynamically-scoped values
Default implRequired (becomes .Default layer)Optional - supplied later
BoilerplateLess - tag + layer generatedMore - build layers yourself

Effect.Service (Preferred for App Code)

Use Effect.Service when you have a sensible default implementation:

import { Effect } from "effect";

export class UserService extends Effect.Service<UserService>()("@app/UserService", {
  accessors: true,
  dependencies: [UserRepo.Default, CacheService.Default],
  effect: Effect.gen(function* () {
    const repo = yield* UserRepo;
    const cache = yield* CacheService;

    const findById = Effect.fn("UserService.findById")(function* (id: UserId) {
      const cached = yield* cache.get(id);
      if (Option.isSome(cached)) return cached.value;
      const user = yield* repo.findById(id);
      yield* cache.set(id, user);
      return user;
    });

    const create = Effect.fn("UserService.create")(function* (data: CreateUserInput) {
      const user = yield* repo.create(data);
      yield* Effect.log("User created", { userId: user.id });
      return user;
    });

    return { findById, create };
  }),
}) {}

// Usage - dependencies automatically wired via .Default
const program = Effect.gen(function* () {
  const user = yield* UserService.findById(userId); // accessors enabled
});

// At app root
const MainLive = Layer.mergeAll(UserService.Default, OtherService.Default);

The class is the tag: You can provide alternate implementations for testing:

const mock = new UserService({ findById: () => Effect.succeed(mockUser) });
program.pipe(Effect.provideService(UserService, mock));

Context.Tag (For Libraries / No Default)

Use Context.Tag when no sensible default exists or you're writing library code:

import { Context, Effect, Layer } from "effect";

// Per-request database handle - no sensible global default
class RequestDb extends Context.Tag("@app/RequestDb")<
  RequestDb,
  { readonly query: (sql: string) => Effect.Effect<unknown[]> }
>() {}

// Library code - callers provide implementation
class PaymentGateway extends Context.Tag("@lib/PaymentGateway")<
  PaymentGateway,
  { readonly charge: (amount: number) => Effect.Effect<Receipt, PaymentError> }
>() {}

// Implement with Layer.effect when needed
const RequestDbLive = Layer.effect(
  RequestDb,
  Effect.gen(function* () {
    const pool = yield* DatabasePool;
    return RequestDb.of({
      query: (sql) => pool.query(sql),
    });
  }),
);

Key rules:

  • Tag identifiers must be unique. Use @path/to/ServiceName prefix pattern
  • Service methods should have no dependencies (R = never)
  • Use readonly properties

See references/service-patterns.md for service-driven development and test layers.

Error Definition Pattern

Use Schema.TaggedError for errors. They are serializable (required for RPC) and yieldable (no need for Effect.fail()):

import { Schema } from "effect";

class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()("UserNotFoundError", {
  userId: UserId,
  userMessage: Schema.String,
}) {}

// Usage - yieldable errors can be used directly
const findUser = Effect.fn("findUser")(function* (id: UserId) {
  const user = yield* repo.findById(id);
  if (Option.isNone(user)) {
    return yield* UserNotFoundError.make({ userId: id, userMessage: "User not found" });
  }
  return user.value;
});

Error Recovery

Use catchTag/catchTags for type-safe error handling:

// Single error type
yield *
  repo
    .findById(id)
    .pipe(
      Effect.catchTag("DatabaseError", (err) =>
        UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
      ),
    );

// Multiple error types
yield *
  effect.pipe(
    Effect.catchTags({
      DatabaseError: (err) => UserLookupError.make({ userId: id, reason: err.reason, cause: err }),
      ValidationError: (err) =>
        InvalidInputError.make({ field: err.field, reason: err.reason, cause: err }),
    }),
  );

Schema.Defect for Unknown Errors

Wrap errors from external libraries with Schema.Defect:

class ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
  endpoint: Schema.String,
  statusCode: Schema.Number,
  error: Schema.Defect, // Wraps unknown errors
}) {}

See references/error-patterns.md for expected vs defects and retry patterns.

Data Modeling

Branded Types

Brand all entity IDs to prevent mixing values with the same underlying type:

import { Schema } from "effect";

export const UserId = Schema.String.pipe(Schema.brand("UserId"));
export type UserId = typeof UserId.Type;

export const PostId = Schema.String.pipe(Schema.brand("PostId"));
export type PostId = typeof PostId.Type;

// Type error: can't pass PostId where UserId expected
function getUser(id: UserId) {
  /* ... */
}
getUser(PostId.make("post-123")); // Error!

Schema.Class for Records

Use Schema.Class for composite data models:

export class User extends Schema.Class<User>("User")({
  id: UserId,
  name: Schema.String,
  email: Schema.String,
  createdAt: Schema.Date,
}) {
  get displayName() {
    return `${this.name} (${this.email})`;
  }
}

Schema.TaggedClass for Variants

Use Schema.TaggedClass with Schema.Union for discriminated unions:

import { Match, Schema } from "effect";

export class Success extends Schema.TaggedClass<Success>()("Success", {
  value: Schema.Number,
}) {}

export class Failure extends Schema.TaggedClass<Failure>()("Failure", {
  error: Schema.String,
}) {}

export const Result = Schema.Union(Success, Failure);

// Pattern match with Match.valueTags
Match.valueTags(result, {
  Success: ({ value }) => `Got: ${value}`,
  Failure: ({ error }) => `Error: ${error}`,
});

See references/schema-patterns.md for JSON encoding and advanced patterns.

Layer Composition & Memoization

Provide layers once at the top of your application:

const appLayer = userServiceLayer.pipe(
  Layer.provideMerge(databaseLayer),
  Layer.provideMerge(loggerLayer),
);

const main = program.pipe(Effect.provide(appLayer));
Effect.runPromise(main);

Layer Memoization Warning

Effect memoizes layers by reference identity. Store parameterized layers in constants:

// BAD: creates TWO connection pools
const badLayer = Layer.merge(
  UserRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))),
  OrderRepo.layer.pipe(Layer.provide(Postgres.layer({ url: "..." }))), // Different reference!
);

// GOOD: single connection pool
const postgresLayer = Postgres.layer({ url: "..." });
const goodLayer = Layer.merge(
  UserRepo.layer.pipe(Layer.provide(postgresLayer)),
  OrderRepo.layer.pipe(Layer.provide(postgresLayer)), // Same reference!
);

See references/layer-patterns.md for test layers and config-dependent layers.

Config

Use Config.* primitives or Schema.Config for type-safe configuration:

import { Config, Effect, Schema } from "effect";

const Port = Schema.Int.pipe(Schema.between(1, 65535));

const program = Effect.gen(function* () {
  // Basic primitives
  const apiKey = yield* Config.redacted("API_KEY");
  const port = yield* Config.integer("PORT");

  // With Schema validation
  const validatedPort = yield* Schema.Config("PORT", Port);
});

Config Service Pattern

Create config services with test layers:

class ApiConfig extends Context.Tag("@app/ApiConfig")<
  ApiConfig,
  { readonly apiKey: Redacted.Redacted; readonly baseUrl: string }
>() {
  static readonly layer = Layer.effect(
    ApiConfig,
    Effect.gen(function* () {
      const apiKey = yield* Config.redacted("API_KEY");
      const baseUrl = yield* Config.string("API_BASE_URL");
      return ApiConfig.of({ apiKey, baseUrl });
    }),
  );

  // For tests - hardcoded values
  static readonly testLayer = Layer.succeed(ApiConfig, {
    apiKey: Redacted.make("test-key"),
    baseUrl: "https://test.example.com",
  });
}

See references/config-patterns.md for ConfigProvider and advanced patterns.

Testing

Use @effect/vitest for Effect-native testing:

import { Effect } from "effect";
import { describe, expect, it } from "@effect/vitest";

describe("Calculator", () => {
  it.effect("adds numbers", () =>
    Effect.gen(function* () {
      const result = yield* Effect.succeed(1 + 1);
      expect(result).toBe(2);
    }),
  );

  // With scoped resources
  it.scoped("cleans up resources", () =>
    Effect.gen(function* () {
      const tempDir = yield* fs.makeTempDirectoryScoped();
      // tempDir deleted when scope closes
    }),
  );
});

Test Layers

Create in-memory test layers with Layer.sync:

class Users extends Context.Tag("@app/Users")<
  Users,
  {
    /* ... */
  }
>() {
  static readonly testLayer = Layer.sync(Users, () => {
    const store = new Map<UserId, User>();

    const create = (user: User) => Effect.sync(() => void store.set(user.id, user));
    const findById = (id: UserId) => Effect.fromNullable(store.get(id));

    return Users.of({ create, findById });
  });
}

See references/testing-patterns.md for TestClock and worked examples.

CLI

Use @effect/cli for typed argument parsing:

import { Args, Command, Options } from "@effect/cli";
import { BunContext, BunRuntime } from "@effect/platform-bun";
import { Console, Effect } from "effect";

const name = Args.text({ name: "name" }).pipe(Args.withDefault("World"));
const shout = Options.boolean("shout").pipe(Options.withAlias("s"));

const greet = Command.make("greet", { name, shout }, ({ name, shout }) => {
  const message = `Hello, ${name}`;
  return Console.log(shout ? message.toUpperCase() : message);
});

const cli = Command.run(greet, { name: "greet", version: "1.0.0" });

cli(process.argv).pipe(Effect.provide(BunContext.layer), BunRuntime.runMain);

See references/cli-patterns.md for subcommands and service integration.

Effect Atom (Frontend State)

Effect Atom provides reactive state management for React with Effect integration.

Basic Atoms

import { Atom } from "@effect-atom/atom-react";

// Define atoms OUTSIDE components
const countAtom = Atom.make(0);

// Use keepAlive for global state that should persist
const userPrefsAtom = Atom.make({ theme: "dark" }).pipe(Atom.keepAlive);

// Atom families for per-entity state
const modalAtomFamily = Atom.family((type: string) =>
  Atom.make({ isOpen: false }).pipe(Atom.keepAlive),
);

React Integration

import { useAtomValue, useAtomSet, useAtom } from "@effect-atom/atom-react"

function Counter() {
  const count = useAtomValue(countAtom)           // Read only
  const setCount = useAtomSet(countAtom)          // Write only
  const [value, setValue] = useAtom(countAtom)    // Read + write

  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>
}

Handling Results with Result.builder

import { Result } from "@effect-atom/atom-react"

function UserProfile() {
  const userResult = useAtomValue(userAtom)

  return Result.builder(userResult)
    .onInitial(() => <div>Loading...</div>)
    .onErrorTag("NotFoundError", () => <div>User not found</div>)
    .onError((error) => <div>Error: {error.message}</div>)
    .onSuccess((user) => <div>Hello, {user.name}</div>)
    .render()
}

See references/effect-atom-patterns.md for complete patterns.

Anti-Patterns (Forbidden)

// FORBIDDEN - runSync/runPromise inside services
yield *
  Effect.gen(function* () {
    const result = Effect.runPromise(someEffect);
  }); // Always prefer yielding the effect. As a workaround for libraries requiring promises etc, extract the current runtime using `const runtime = yield* Effect.runtime<never>();` then use it to run the promise.

// FORBIDDEN - throw inside Effect.gen
yield *
  Effect.gen(function* () {
    if (bad) throw new Error("No!"); // Use Effect.fail or yieldable error
  });

// FORBIDDEN - catchAll losing type info
yield * effect.pipe(Effect.catchAll(() => Effect.fail(new GenericError())));

// FORBIDDEN - console.log
console.log("debug"); // Use Effect.log

// FORBIDDEN - process.env directly
const key = process.env.API_KEY; // Use Config.string("API_KEY")

// FORBIDDEN - null/undefined in domain types
type User = { name: string | null }; // Use Option<string>

See references/anti-patterns.md for the complete list with rationale.

Reference Files / How to use

For detailed patterns or in the case of any ambiguity you must consult these reference files in the references/ directory:

  • anti-patterns.md - Complete list of forbidden patterns
  • cli-patterns.md - @effect/cli Commands, Args, Options, subcommands
  • config-patterns.md - Config primitives, Schema.Config, ConfigProvider
  • domain-predicates.md - Equivalence, Order, Schema.Data for equality and sorting
  • effect-atom-patterns.md - Atom, families, React hooks, Result handling
  • error-patterns.md - Schema.TaggedError, yieldable errors, Schema.Defect
  • layer-patterns.md - Dependency composition, memoization, testing layers
  • observability-patterns.md - Logging, metrics, config patterns
  • rpc-cluster-patterns.md - RpcGroup, Workflow, Activity patterns
  • schema-patterns.md - Branded types, Schema.Class, JSON encoding
  • service-patterns.md - Effect.Service vs Context.Tag, dependencies, test layers
  • testing-patterns.md - @effect/vitest, it.effect, it.scoped, TestClock, property-based testing

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

Self Updater

⭐ OPEN SOURCE! GitHub: github.com/GhostDragon124/openclaw-self-updater ⭐ ONLY skill with Cron-aware + Idle detection! Auto-updates OpenClaw core & skills, an...

Registry SourceRecently Updated
1221Profile unavailable
Coding

ClawHub CLI Assistant

Use the ClawHub CLI to publish, inspect, version, update, sync, and troubleshoot OpenClaw skills from the terminal.

Registry SourceRecently Updated
1.9K2Profile unavailable
Coding

SkillTree Learning Progress Tracker

Track learning across topics like an RPG skill tree. Prerequisites, milestones, suggested next steps. Gamified learning path.

Registry SourceRecently Updated
900Profile unavailable
Coding

Speak Turbo - Talk to your Claude 90ms latency!

Give your agent the ability to speak to you real-time. Talk to your Claude! Ultra-fast TTS, text-to-speech, voice synthesis, audio output with ~90ms latency....

Registry SourceRecently Updated
4480Profile unavailable