Loading...
Loading...
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.
npx skill4agent add stromseng/skills effective-effectEffect<Success, Error, Requirements>
// ↑ ↑ ↑
// | | └── Dependencies (provided via Layers)
// | └── Expected errors (typed, must be handled)
// └── Success valueNotFoundErrorBadRequestError// 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
}) {}UserNotFoundErroruserIdSessionExpiredErrorexpiredAtcatchTagcatchTagsmessageEffect.logexpiredAtuserMessagecauseSchema.Defect| Category | DO | DON'T |
|---|---|---|
| Services (app) | | Inline dependencies in methods |
| Services (lib) | | Assuming implementation in library code |
| Dependencies | | Pass services as function parameters |
| Errors | | Plain classes or generic Error |
| Error Recovery | | |
| IDs | | Plain |
| Functions | | Anonymous generators |
| Sequencing | | Nested |
| Logging | | |
| Config | | |
| Options | | |
| Nullability | | |
| Test Layers | | Mocking frameworks |
| Atoms | | Creating atoms inside render |
| Atom Results | | Ignoring loading/error states |
async/awaitPromiseEffect.genyield*Effectimport { Effect } from "effect";
const program = Effect.gen(function* () {
const data = yield* fetchData;
yield* Effect.logInfo(`Processing data: ${data}`);
return yield* processData(data);
});Effect.fnEffect.fnimport { 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);
});.pipe()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"),
);Effect.timeoutEffect.retryEffect.tapEffect.withSpanEffect.ServiceContext.Tag| Feature | Effect.Service | Context.Tag |
|---|---|---|
| Best for | Application code with clear implementation | Library code or dynamically-scoped values |
| Default impl | Required (becomes | Optional - supplied later |
| Boilerplate | Less - tag + layer generated | More - build layers yourself |
Effect.Serviceimport { 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);const mock = new UserService({ findById: () => Effect.succeed(mockUser) });
program.pipe(Effect.provideService(UserService, mock));Context.Tagimport { 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),
});
}),
);@path/to/ServiceNameR = neverreferences/service-patterns.mdSchema.TaggedErrorEffect.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;
});catchTagcatchTags// 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.Defectclass ApiError extends Schema.TaggedError<ApiError>()("ApiError", {
endpoint: Schema.String,
statusCode: Schema.Number,
error: Schema.Defect, // Wraps unknown errors
}) {}references/error-patterns.mdimport { 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.Classexport 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.TaggedClassSchema.Unionimport { 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}`,
});references/schema-patterns.mdconst appLayer = userServiceLayer.pipe(
Layer.provideMerge(databaseLayer),
Layer.provideMerge(loggerLayer),
);
const main = program.pipe(Effect.provide(appLayer));
Effect.runPromise(main);// 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!
);references/layer-patterns.mdConfig.*Schema.Configimport { 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);
});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",
});
}references/config-patterns.md@effect/vitestimport { 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
}),
);
});Layer.syncclass 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 });
});
}references/testing-patterns.md@effect/cliimport { 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);references/cli-patterns.mdimport { 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),
);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>
}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()
}references/effect-atom-patterns.md// 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>references/anti-patterns.mdreferences/anti-patterns.mdcli-patterns.mdconfig-patterns.mddomain-predicates.mdeffect-atom-patterns.mderror-patterns.mdlayer-patterns.mdobservability-patterns.mdrpc-cluster-patterns.mdschema-patterns.mdservice-patterns.mdtesting-patterns.md