| name | effect-best-practices |
| description | Use when you are implementing features using Effect.ts |
Effect.ts Patterns
Core Principles
Effect Type Signature
Effect<Success, Error, Requirements>;
Never Use throw
Always use Effect.fail with TaggedErrors. Native exceptions become "defects" (unrecoverable).
throw new Error("User not found");
Effect.fail(new UserNotFoundError({ userId }));
TaggedErrors should be defined using Data.TaggedError:
import { Data } from "effect";
export class NotFoundError extends Data.TaggedError("NotFoundError")<{
resourceId: string;
}> {}
export class ValidationError extends Data.TaggedError("ValidationError")<{
field: string;
message: string;
}> {}
Generator Syntax (Effect.gen)
Prefer Effect.gen with generator functions for imperative-style code:
import { Effect } from "effect";
const program = Effect.gen(function* () {
const user = yield* getUser(userId);
const profile = yield* getProfile(user.profileId);
return { user, profile };
});
Key Rules:
- Use
yield* (not yield) to unwrap Effects
- The function must be a generator (
function*)
- Return value becomes the Effect's success value
Error Handling
Tagged Errors Pattern
import { Data, Effect } from "effect";
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
userId: string;
}> {}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
cause: unknown;
}> {}
const program = Effect.gen(function* () {
const user = yield* getUser(userId);
}).pipe(
Effect.catchTag(
"UserNotFoundError",
(error) => Effect.succeed(null),
),
);
Effect.catchTags(effect, {
UserNotFoundError: (e) => Effect.succeed(null),
DatabaseError: (e) => Effect.fail(new ServiceError({ cause: e })),
});
Error Mapping
Effect.mapError(effect, (error) => new WrapperError({ cause: error }));
Effect.orDie(effect);
Effect.orElseSucceed(effect, () => defaultValue);
Logging
Use Console from Effect instead of native console:
import { Console, Effect } from "effect";
Effect.gen(function* () {
yield* Console.log("Processing user:", userId);
yield* Console.warn("Deprecated method called");
yield* Console.error("Failed to process");
});
Services and Dependency Injection
Defining Services with Context.Tag
import { Context, Effect, Layer } from "effect";
class UserService extends Context.Tag("UserService")<
UserService,
{
readonly findById: (id: string) => Effect.Effect<User, NotFoundError>;
readonly create: (
data: CreateUserInput,
) => Effect.Effect<User, ValidationError>;
}
>() {}
class UserService extends Effect.Service<UserService>()("UserService", {
effect: Effect.gen(function* () {
const db = yield* Database;
const findById = (id: string) =>
Effect.gen(function* () {
const user = yield* db.query("SELECT * FROM users WHERE id = ?", [id]);
if (!user) return yield* Effect.fail(new NotFoundError({ id }));
return user;
});
return { findById } as const;
}),
}) {}
Creating Layers
import { Layer } from "effect";
const ConfigLive = Layer.succeed(Config, { apiUrl: "https://api.example.com" });
const UserServiceLive = Layer.effect(
UserService,
Effect.gen(function* () {
const db = yield* Database;
return {
findById: (id) => db.find("users", id),
};
}),
);
const ConnectionLive = Layer.scoped(
Connection,
Effect.acquireRelease(
Effect.promise(() => createConnection()),
(conn) => Effect.sync(() => conn.close()),
),
);
const AppLive = Layer.mergeAll(
ConfigLive,
UserServiceLive.pipe(Layer.provide(DatabaseLive)),
);
Schema Validation
Use Schema from Effect for validation (not Zod):
import { Schema } from "effect";
const User = Schema.Struct({
id: Schema.String,
email: Schema.String.pipe(Schema.nonEmptyString()),
age: Schema.Number.pipe(Schema.int(), Schema.positive()),
role: Schema.Literal("admin", "user"),
});
type User = Schema.Schema.Type<typeof User>;
const parseUser = Schema.decodeUnknown(User);
const result = parseUser(data);
Effect.gen(function* () {
const user = yield* Schema.decode(User)(rawData);
});
const UserFromJSON = Schema.transform(Schema.String, User, {
decode: (s) => JSON.parse(s),
encode: (u) => JSON.stringify(u),
});
Common Patterns
Running Effects
Effect.runSync(effect);
await Effect.runPromise(effect);
const exit = Effect.runSyncExit(effect);
const exit = await Effect.runPromiseExit(effect);
Effect Constructors
Effect.succeed(value);
Effect.fail(new MyError());
Effect.sync(() => computeValue());
Effect.try({
try: () => JSON.parse(str),
catch: (error) => new ParseError({ cause: error }),
});
Effect.promise(() => fetch(url));
Effect.tryPromise({
try: () => fetch(url),
catch: (error) => new NetworkError({ cause: error }),
});
Combining Effects
const [a, b] = yield * Effect.all([effectA, effectB]);
const { user, profile } =
yield *
Effect.all({
user: getUser(id),
profile: getProfile(id),
});
Effect.all([effectA, effectB], { concurrency: "unbounded" });
Effect.race([effect1, effect2]);
Effect.all(effects, { mode: "either" });
Transformations
Effect.map(effect, (a) => a * 2);
Effect.flatMap(effect, (a) => getNextEffect(a));
Effect.tap(effect, (a) => Console.log("Got:", a));
effect.pipe(Effect.provideService(MyService, implementation));
Resource Management
Effect.acquireUseRelease(
Effect.sync(() => openFile(path)),
(file) => Effect.sync(() => readFile(file)),
(file) => Effect.sync(() => closeFile(file)),
);
Effect.scoped(
Effect.gen(function* () {
const file = yield* Effect.acquireRelease(openFile(path), (f) =>
closeFile(f),
);
return yield* readFile(file);
}),
);
Retries
import { Schedule } from "effect";
Effect.retry(effect, Schedule.recurs(3));
Effect.retry(
effect,
Schedule.exponential("100 millis").pipe(Schedule.compose(Schedule.recurs(5))),
);
Effect.retry(effect, {
schedule: Schedule.recurs(3),
while: (error) => error._tag === "NetworkError",
});
Timeouts
import { Duration } from "effect";
Effect.timeout(effect, "5 seconds");
Effect.timeoutTo(effect, {
duration: "5 seconds",
onTimeout: Effect.succeed(defaultValue),
});
Testing with Effect
import { it } from "@effect/vitest";
it.effect("should create user", () =>
Effect.gen(function* () {
const result = yield* createUser(input);
expect(result.id).toBeDefined();
}),
);
it.effect("should work with services", () =>
Effect.gen(function* () {
const service = yield* MyService;
const result = yield* service.doSomething();
expect(result).toBe(expected);
}).pipe(Effect.provide(TestLayer)),
);
Anti-Patterns to Avoid
console.log("debug");
yield * Console.log("debug");
if (!user) throw new Error("User not found");
if (!user) return yield * Effect.fail(new UserNotFoundError({ id }));
try {
await doSomething();
} catch (e) {
handleError(e);
}
Effect.tryPromise({
try: () => doSomething(),
catch: (e) => new MyError({ cause: e }),
}).pipe(Effect.catchTag("MyError", handleError));
async function bad() {
const result = await Effect.runPromise(effect);
return result;
}
const good = Effect.gen(function* () {
const result = yield* effect;
return result;
});
Effect.gen(function* () {
const service = { method: () => Effect.succeed(1) };
});
const ServiceLive = Layer.succeed(Service, { method: () => Effect.succeed(1) });