# Intermediate Level Rules ## Access Configuration from the Context **Rule:** Access configuration from the Effect context. ### Example ```typescript import { Config, Effect, Layer } from "effect"; // Define config service class AppConfig extends Effect.Service()("AppConfig", { sync: () => ({ host: "localhost", port: 3000, }), }) {} // Create program that uses config const program = Effect.gen(function* () { const config = yield* AppConfig; yield* Effect.log(`Starting server on http://${config.host}:${config.port}`); }); // Run the program with default config Effect.runPromise(Effect.provide(program, AppConfig.Default)); ``` **Explanation:** By yielding the config object, you make your dependency explicit and leverage Effect's context system for testability and modularity. ## Accessing the Current Time with Clock **Rule:** Use the Clock service to get the current time, enabling deterministic testing with TestClock. ### Example This example shows a function that checks if a token is expired. Its logic depends on `Clock`, making it fully testable. ```typescript import { Effect, Clock, Duration } from "effect"; interface Token { readonly value: string; readonly expiresAt: number; // UTC milliseconds } // This function is pure and testable because it depends on Clock const isTokenExpired = ( token: Token ): Effect.Effect => Clock.currentTimeMillis.pipe( Effect.map((now) => now > token.expiresAt), Effect.tap((expired) => Effect.log( `Token expired? ${expired} (current time: ${new Date().toISOString()})` ) ) ); // Create a test clock service that advances time const makeTestClock = (timeMs: number): Clock.Clock => ({ currentTimeMillis: Effect.succeed(timeMs), currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)), sleep: (duration: Duration.Duration) => Effect.succeed(void 0), unsafeCurrentTimeMillis: () => timeMs, unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000), [Clock.ClockTypeId]: Clock.ClockTypeId, }); // Create a token that expires in 1 second const token = { value: "abc", expiresAt: Date.now() + 1000 }; // Check token expiry with different clocks const program = Effect.gen(function* () { // Check with current time yield* Effect.log("Checking with current time..."); yield* isTokenExpired(token); // Check with past time yield* Effect.log("\nChecking with past time (1 minute ago)..."); const pastClock = makeTestClock(Date.now() - 60_000); yield* isTokenExpired(token).pipe( Effect.provideService(Clock.Clock, pastClock) ); // Check with future time yield* Effect.log("\nChecking with future time (1 hour ahead)..."); const futureClock = makeTestClock(Date.now() + 3600_000); yield* isTokenExpired(token).pipe( Effect.provideService(Clock.Clock, futureClock) ); }); // Run the program with default clock Effect.runPromise( program.pipe(Effect.provideService(Clock.Clock, makeTestClock(Date.now()))) ); ``` --- ## Accumulate Multiple Errors with Either **Rule:** Use Either to accumulate multiple validation errors instead of failing on the first one. ### Example Using `Schema.decode` with the `allErrors: true` option demonstrates this pattern perfectly. The underlying mechanism uses `Either` to collect all parsing errors into an array instead of stopping at the first one. ```typescript import { Effect, Schema, Data } from "effect"; // Define validation error type class ValidationError extends Data.TaggedError("ValidationError")<{ readonly field: string; readonly message: string; }> {} // Define user type type User = { name: string; email: string; }; // Define schema with custom validation const UserSchema = Schema.Struct({ name: Schema.String.pipe( Schema.minLength(3), Schema.filter((name) => /^[A-Za-z\s]+$/.test(name), { message: () => "name must contain only letters and spaces", }) ), email: Schema.String.pipe( Schema.pattern(/@/), Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { message: () => "email must be a valid email address", }) ), }); // Example inputs const invalidInputs: User[] = [ { name: "Al", // Too short email: "bob-no-at-sign.com", // Invalid pattern }, { name: "John123", // Contains numbers email: "john@incomplete", // Invalid email }, { name: "Alice Smith", // Valid email: "alice@example.com", // Valid }, ]; // Validate a single user const validateUser = (input: User) => Effect.gen(function* () { const result = yield* Schema.decode(UserSchema)(input, { errors: "all" }); return result; }); // Process multiple users and accumulate all errors const program = Effect.gen(function* () { console.log("Validating users...\n"); for (const input of invalidInputs) { const result = yield* Effect.either(validateUser(input)); console.log(`Validating user: ${input.name} <${input.email}>`); yield* Effect.match(result, { onFailure: (error) => Effect.sync(() => { console.log("❌ Validation failed:"); console.log(error.message); console.log(); // Empty line for readability }), onSuccess: (user) => Effect.sync(() => { console.log("✅ User is valid:", user); console.log(); // Empty line for readability }), }); } }); // Run the program Effect.runSync(program); ``` --- ## Add Custom Metrics to Your Application **Rule:** Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring. ### Example This example creates a counter to track how many times a user is created and a histogram to track the duration of the database operation. ```typescript import { Effect, Metric, Duration } from "effect"; // We don't need MetricBoundaries anymore // 1. Define your metrics const userRegisteredCounter = Metric.counter("users_registered_total", { description: "A counter for how many users have been registered.", }); const dbDurationTimer = Metric.timer( "db_operation_duration", "A timer for DB operation durations" ); // 2. Simulated database call const saveUserToDb = Effect.succeed("user saved").pipe( Effect.delay(Duration.millis(Math.random() * 100)) ); // 3. Instrument the business logic const createUser = Effect.gen(function* () { // Time the operation yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer)); // Increment the counter yield* Metric.increment(userRegisteredCounter); return { status: "success" }; }); // Run the Effect Effect.runPromise(createUser).then(console.log); ``` --- ## Automatically Retry Failed Operations **Rule:** Compose a Stream with the .retry(Schedule) operator to automatically recover from transient failures. ### Example This example simulates an API that fails the first two times it's called. The stream processes a list of IDs, and the `retry` operator ensures that the failing operation for `id: 2` is automatically retried until it succeeds. ```typescript import { Effect, Stream, Schedule } from "effect"; // A mock function that simulates a flaky API call const processItem = (id: number): Effect.Effect => Effect.gen(function* () { yield* Effect.log(`Attempting to process item ${id}...`); // Item 2 fails on first attempt but succeeds on retry if (id === 2) { const random = Math.random(); if (random < 0.5) { // 50% chance of failure for demonstration yield* Effect.log(`Item ${id} failed, will retry...`); return yield* Effect.fail(new Error("API is temporarily down")); } } yield* Effect.log(`✅ Successfully processed item ${id}`); return `Processed item ${id}`; }); const ids = [1, 2, 3]; // Define a retry policy: 3 attempts with a fixed 100ms delay const retryPolicy = Schedule.recurs(3).pipe( Schedule.addDelay(() => "100 millis") ); const program = Effect.gen(function* () { yield* Effect.log("=== Stream Retry on Failure Demo ==="); yield* Effect.log( "Processing items with retry policy (3 attempts, 100ms delay)" ); // Process each item individually with retry const results = yield* Effect.forEach( ids, (id) => processItem(id).pipe( Effect.retry(retryPolicy), Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.log( `❌ Item ${id} failed after all retries: ${error.message}` ); return `Failed: item ${id}`; }) ) ), { concurrency: 1 } ); yield* Effect.log("=== Results ==="); results.forEach((result, index) => { console.log(`Item ${ids[index]}: ${result}`); }); yield* Effect.log("✅ Stream processing completed"); }); Effect.runPromise(program).catch((error) => { console.error("Unexpected error:", error); }); /* Output: ... level=INFO msg="Attempting to process item 1..." ... level=INFO msg="Attempting to process item 2..." ... level=INFO msg="Item 2 failed, attempt 1." ... level=INFO msg="Attempting to process item 2..." ... level=INFO msg="Item 2 failed, attempt 2." ... level=INFO msg="Attempting to process item 2..." ... level=INFO msg="Attempting to process item 3..." */ ``` ## Avoid Long Chains of .andThen; Use Generators Instead **Rule:** Prefer generators over long chains of .andThen. ### Example ```typescript import { Effect } from "effect"; // Define our steps with logging const step1 = (): Effect.Effect => Effect.succeed(42).pipe(Effect.tap((n) => Effect.log(`Step 1: ${n}`))); const step2 = (a: number): Effect.Effect => Effect.succeed(`Result: ${a * 2}`).pipe( Effect.tap((s) => Effect.log(`Step 2: ${s}`)) ); // Using Effect.gen for better readability const program = Effect.gen(function* () { const a = yield* step1(); const b = yield* step2(a); return b; }); // Run the program Effect.runPromise(program).then((result) => Effect.runSync(Effect.log(`Final result: ${result}`)) ); ``` **Explanation:** Generators keep sequential logic readable and easy to maintain. ## Beyond the Date Type - Real World Dates, Times, and Timezones **Rule:** Use the Clock service for testable time-based logic and immutable primitives for timestamps. ### Example This example shows a function that creates a timestamped event. It depends on the `Clock` service, making it fully testable. ```typescript import { Effect, Clock } from "effect"; import type * as Types from "effect/Clock"; interface Event { readonly message: string; readonly timestamp: number; // Store as a primitive number (UTC millis) } // This function is pure and testable because it depends on Clock const createEvent = ( message: string ): Effect.Effect => Effect.gen(function* () { const timestamp = yield* Clock.currentTimeMillis; return { message, timestamp }; }); // Create and log some events const program = Effect.gen(function* () { const loginEvent = yield* createEvent("User logged in"); console.log("Login event:", loginEvent); const logoutEvent = yield* createEvent("User logged out"); console.log("Logout event:", logoutEvent); }); // Run the program Effect.runPromise( program.pipe(Effect.provideService(Clock.Clock, Clock.make())) ).catch(console.error); ``` --- ## Compose Resource Lifecycles with `Layer.merge` **Rule:** Compose multiple scoped layers using `Layer.merge` or by providing one layer to another. ### Example ```typescript import { Effect, Layer, Console } from "effect"; // --- Service 1: Database --- interface DatabaseOps { query: (sql: string) => Effect.Effect; } class Database extends Effect.Service()("Database", { sync: () => ({ query: (sql: string): Effect.Effect => Effect.sync(() => `db says: ${sql}`), }), }) {} // --- Service 2: API Client --- interface ApiClientOps { fetch: (path: string) => Effect.Effect; } class ApiClient extends Effect.Service()("ApiClient", { sync: () => ({ fetch: (path: string): Effect.Effect => Effect.sync(() => `api says: ${path}`), }), }) {} // --- Application Layer --- // We merge the two independent layers into one. const AppLayer = Layer.merge(Database.Default, ApiClient.Default); // This program uses both services, unaware of their implementation details. const program = Effect.gen(function* () { const db = yield* Database; const api = yield* ApiClient; const dbResult = yield* db.query("SELECT *"); const apiResult = yield* api.fetch("/users"); yield* Console.log(dbResult); yield* Console.log(apiResult); }); // Provide the combined layer to the program. Effect.runPromise(Effect.provide(program, AppLayer)); /* Output (note the LIFO release order): Database pool opened API client session started db says: SELECT * api says: /users API client session ended Database pool closed */ ``` **Explanation:** We define two completely independent services, `Database` and `ApiClient`, each with its own resource lifecycle. By combining them with `Layer.merge`, we create a single `AppLayer`. When `program` runs, Effect acquires the resources for both layers. When `program` finishes, Effect closes the application's scope, releasing the resources in the reverse order they were acquired (`ApiClient` then `Database`), ensuring a clean and predictable shutdown. ## Conditionally Branching Workflows **Rule:** Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching. ### Example Here, we use `Effect.filterOrFail` with named predicates to validate a user before proceeding. The intent is crystal clear, and the business rules (`isActive`, `isAdmin`) are reusable. ```typescript import { Effect } from "effect"; interface User { id: number; status: "active" | "inactive"; roles: string[]; } type UserError = "DbError" | "UserIsInactive" | "UserIsNotAdmin"; const findUser = (id: number): Effect.Effect => Effect.succeed({ id, status: "active", roles: ["admin"] }); // Reusable, testable predicates that document business rules. const isActive = (user: User): boolean => user.status === "active"; const isAdmin = (user: User): boolean => user.roles.includes("admin"); const program = (id: number): Effect.Effect => findUser(id).pipe( // Validate user is active using Effect.filterOrFail Effect.filterOrFail(isActive, () => "UserIsInactive" as const), // Validate user is admin using Effect.filterOrFail Effect.filterOrFail(isAdmin, () => "UserIsNotAdmin" as const), // Success case Effect.map((user) => `Welcome, admin user #${user.id}!`) ); // We can then handle the specific failures in a type-safe way. const handled = program(123).pipe( Effect.match({ onFailure: (error) => { switch (error) { case "UserIsNotAdmin": return "Access denied: requires admin role."; case "UserIsInactive": return "Access denied: user is not active."; case "DbError": return "Error: could not find user."; default: return `Unknown error: ${error}`; } }, onSuccess: (result) => result, }) ); // Run the program Effect.runPromise(handled).then(console.log); ``` --- ## Control Flow with Conditional Combinators **Rule:** Use conditional combinators for control flow. ### Example ```typescript import { Effect } from "effect"; const attemptAdminAction = (user: { isAdmin: boolean }) => Effect.if(user.isAdmin, { onTrue: () => Effect.succeed("Admin action completed."), onFalse: () => Effect.fail("Permission denied."), }); const program = Effect.gen(function* () { // Try with admin user yield* Effect.logInfo("\nTrying with admin user..."); const adminResult = yield* Effect.either( attemptAdminAction({ isAdmin: true }) ); yield* Effect.logInfo( `Admin result: ${adminResult._tag === "Right" ? adminResult.right : adminResult.left}` ); // Try with non-admin user yield* Effect.logInfo("\nTrying with non-admin user..."); const userResult = yield* Effect.either( attemptAdminAction({ isAdmin: false }) ); yield* Effect.logInfo( `User result: ${userResult._tag === "Right" ? userResult.right : userResult.left}` ); }); Effect.runPromise(program); ``` **Explanation:** `Effect.if` and related combinators allow you to branch logic without leaving the Effect world or breaking the flow of composition. ## Control Repetition with Schedule **Rule:** Use Schedule to create composable policies for controlling the repetition and retrying of effects. ### Example This example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts. ```typescript import { Effect, Schedule, Duration } from "effect"; // A simple effect that can fail const flakyEffect = Effect.try({ try: () => { if (Math.random() > 0.2) { throw new Error("Transient error"); } return "Operation succeeded!"; }, catch: (error: unknown) => { Effect.logInfo("Operation failed, retrying..."); return error; }, }); // --- Building a Composable Schedule --- // 1. Start with a base exponential backoff (100ms, 200ms, 400ms...) const exponentialBackoff = Schedule.exponential("100 millis"); // 2. Add random jitter to avoid thundering herd problems const withJitter = Schedule.jittered(exponentialBackoff); // 3. Limit the schedule to a maximum of 5 repetitions const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5)); // --- Using the Schedule --- const program = Effect.gen(function* () { yield* Effect.logInfo("Starting operation..."); const result = yield* Effect.retry(flakyEffect, limitedWithJitter); yield* Effect.logInfo(`Final result: ${result}`); }); // Run the program Effect.runPromise(program); ``` --- ## Create a Service Layer from a Managed Resource **Rule:** Provide a managed resource to the application context using `Layer.scoped`. ### Example ```typescript import { Effect, Console } from "effect"; // 1. Define the service interface interface DatabaseService { readonly query: (sql: string) => Effect.Effect; } // 2. Define the service implementation with scoped resource management class Database extends Effect.Service()("Database", { // The scoped property manages the resource lifecycle scoped: Effect.gen(function* () { const id = Math.floor(Math.random() * 1000); // Acquire the connection yield* Console.log(`[Pool ${id}] Acquired`); // Setup cleanup to run when scope closes yield* Effect.addFinalizer(() => Console.log(`[Pool ${id}] Released`)); // Return the service implementation return { query: (sql: string) => Effect.sync(() => [`Result for '${sql}' from pool ${id}`]), }; }), }) {} // 3. Use the service in your program const program = Effect.gen(function* () { const db = yield* Database; const users = yield* db.query("SELECT * FROM users"); yield* Console.log(`Query successful: ${users[0]}`); }); // 4. Run the program with scoped resource management Effect.runPromise( Effect.scoped(program).pipe(Effect.provide(Database.Default)) ); /* Output: [Pool 458] Acquired Query successful: Result for 'SELECT * FROM users' from pool 458 [Pool 458] Released */ ``` **Explanation:** The `Effect.Service` helper creates the `Database` class with a `scoped` implementation. When `program` asks for the `Database` service, the Effect runtime creates a new connection pool, logs the acquisition, and automatically releases it when the scope closes. The `scoped` implementation ensures proper resource lifecycle management - the pool is acquired when first needed and released when the scope ends. ## Create a Testable HTTP Client Service **Rule:** Define an HttpClient service with distinct Live and Test layers to enable testable API interactions. ### Example ### 1. Define the Service ```typescript import { Effect, Data, Layer } from "effect"; interface HttpErrorType { readonly _tag: "HttpError"; readonly error: unknown; } const HttpError = Data.tagged("HttpError"); interface HttpClientType { readonly get: (url: string) => Effect.Effect; } class HttpClient extends Effect.Service()("HttpClient", { sync: () => ({ get: (url: string): Effect.Effect => Effect.tryPromise({ try: () => fetch(url).then((res) => res.json()), catch: (error) => HttpError({ error }), }), }), }) {} // Test implementation const TestLayer = Layer.succeed( HttpClient, HttpClient.of({ get: (_url: string) => Effect.succeed({ title: "Mock Data" } as T), }) ); // Example usage const program = Effect.gen(function* () { const client = yield* HttpClient; yield* Effect.logInfo("Fetching data..."); const data = yield* client.get<{ title: string }>( "https://api.example.com/data" ); yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`); }); // Run with test implementation Effect.runPromise(Effect.provide(program, TestLayer)); ``` ### 2. Create the Live Implementation ```typescript import { Effect, Data, Layer } from "effect"; interface HttpErrorType { readonly _tag: "HttpError"; readonly error: unknown; } const HttpError = Data.tagged("HttpError"); interface HttpClientType { readonly get: (url: string) => Effect.Effect; } class HttpClient extends Effect.Service()("HttpClient", { sync: () => ({ get: (url: string): Effect.Effect => Effect.tryPromise({ try: () => fetch(url).then((res) => res.json()), catch: (error) => HttpError({ error }), }), }), }) {} // Test implementation const TestLayer = Layer.succeed( HttpClient, HttpClient.of({ get: (_url: string) => Effect.succeed({ title: "Mock Data" } as T), }) ); // Example usage const program = Effect.gen(function* () { const client = yield* HttpClient; yield* Effect.logInfo("Fetching data..."); const data = yield* client.get<{ title: string }>( "https://api.example.com/data" ); yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`); }); // Run with test implementation Effect.runPromise(Effect.provide(program, TestLayer)); ``` ### 3. Create the Test Implementation ```typescript // src/services/HttpClientTest.ts import { Effect, Layer } from "effect"; import { HttpClient } from "./HttpClient"; export const HttpClientTest = Layer.succeed( HttpClient, HttpClient.of({ get: (url) => Effect.succeed({ mock: "data", url }), }) ); ``` ### 4. Usage in Business Logic Your business logic is now clean and only depends on the abstract `HttpClient`. ```typescript // src/features/User/UserService.ts import { Effect } from "effect"; import { HttpClient } from "../../services/HttpClient"; export const getUserFromApi = (id: number) => Effect.gen(function* () { const client = yield* HttpClient; const data = yield* client.get(`https://api.example.com/users/${id}`); // ... logic to parse and return user return data; }); ``` --- ## Define a Type-Safe Configuration Schema **Rule:** Define a type-safe configuration schema. ### Example ```typescript import { Config, Effect, ConfigProvider, Layer } from "effect"; const ServerConfig = Config.nested("SERVER")( Config.all({ host: Config.string("HOST"), port: Config.number("PORT"), }) ); // Example program that uses the config const program = Effect.gen(function* () { const config = yield* ServerConfig; yield* Effect.logInfo(`Server config loaded: ${JSON.stringify(config)}`); }); // Create a config provider with test values const TestConfig = ConfigProvider.fromMap( new Map([ ["SERVER.HOST", "localhost"], ["SERVER.PORT", "3000"], ]) ); // Run with test config Effect.runPromise(Effect.provide(program, Layer.setConfigProvider(TestConfig))); ``` **Explanation:** This schema ensures that both `host` and `port` are present and properly typed, and that their source is clearly defined. ## Define Contracts Upfront with Schema **Rule:** Define contracts upfront with schema. ### Example ```typescript import { Schema, Effect, Data } from "effect"; // Define User schema and type const UserSchema = Schema.Struct({ id: Schema.Number, name: Schema.String, }); type User = Schema.Schema.Type; // Define error type class UserNotFound extends Data.TaggedError("UserNotFound")<{ readonly id: number; }> {} // Create database service implementation export class Database extends Effect.Service()("Database", { sync: () => ({ getUser: (id: number) => id === 1 ? Effect.succeed({ id: 1, name: "John" }) : Effect.fail(new UserNotFound({ id })), }), }) {} // Create a program that demonstrates schema and error handling const program = Effect.gen(function* () { const db = yield* Database; // Try to get an existing user yield* Effect.logInfo("Looking up user 1..."); const user1 = yield* db.getUser(1); yield* Effect.logInfo(`Found user: ${JSON.stringify(user1)}`); // Try to get a non-existent user yield* Effect.logInfo("\nLooking up user 999..."); yield* Effect.logInfo("Attempting to get user 999..."); yield* Effect.gen(function* () { const user = yield* db.getUser(999); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); }).pipe( Effect.catchAll((error) => { if (error instanceof UserNotFound) { return Effect.logInfo(`Error: User with id ${error.id} not found`); } return Effect.logInfo(`Unexpected error: ${error}`); }) ); // Try to decode invalid data yield* Effect.logInfo("\nTrying to decode invalid user data..."); const invalidUser = { id: "not-a-number", name: 123 } as any; yield* Effect.gen(function* () { const user = yield* Schema.decode(UserSchema)(invalidUser); yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`); }).pipe( Effect.catchAll((error) => Effect.logInfo(`Validation failed:\n${JSON.stringify(error, null, 2)}`) ) ); }); // Run the program Effect.runPromise(Effect.provide(program, Database.Default)); ``` **Explanation:** Defining schemas upfront clarifies your contracts and ensures both type safety and runtime validation. ## Define Type-Safe Errors with Data.TaggedError **Rule:** Define type-safe errors with Data.TaggedError. ### Example ```typescript import { Data, Effect } from "effect"; // Define our tagged error type class DatabaseError extends Data.TaggedError("DatabaseError")<{ readonly cause: unknown; }> {} // Function that simulates a database error const findUser = ( id: number ): Effect.Effect<{ id: number; name: string }, DatabaseError> => Effect.gen(function* () { if (id < 0) { return yield* Effect.fail(new DatabaseError({ cause: "Invalid ID" })); } return { id, name: `User ${id}` }; }); // Create a program that demonstrates error handling const program = Effect.gen(function* () { // Try to find a valid user yield* Effect.logInfo("Looking up user 1..."); yield* Effect.gen(function* () { const user = yield* findUser(1); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); }).pipe( Effect.catchAll((error) => Effect.logInfo(`Error finding user: ${error._tag} - ${error.cause}`) ) ); // Try to find an invalid user yield* Effect.logInfo("\nLooking up user -1..."); yield* Effect.gen(function* () { const user = yield* findUser(-1); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); }).pipe( Effect.catchTag("DatabaseError", (error) => Effect.logInfo(`Database error: ${error._tag} - ${error.cause}`) ) ); }); // Run the program Effect.runPromise(program); ``` **Explanation:** Tagged errors allow you to handle errors in a type-safe, self-documenting way. ## Distinguish 'Not Found' from Errors **Rule:** Use Effect> to distinguish between recoverable 'not found' cases and actual failures. ### Example This function to find a user can fail if the database is down, or it can succeed but find no user. The return type `Effect.Effect, DatabaseError>` makes this contract perfectly clear. ```typescript import { Effect, Option, Data } from "effect"; interface User { id: number; name: string; } class DatabaseError extends Data.TaggedError("DatabaseError") {} // This signature is extremely honest about its possible outcomes. const findUserInDb = ( id: number ): Effect.Effect, DatabaseError> => Effect.gen(function* () { // This could fail with a DatabaseError const dbResult = yield* Effect.try({ try: () => (id === 1 ? { id: 1, name: "Paul" } : null), catch: () => new DatabaseError(), }); // We wrap the potentially null result in an Option return Option.fromNullable(dbResult); }); // The caller can now handle all three cases explicitly. const program = (id: number) => findUserInDb(id).pipe( Effect.flatMap((maybeUser) => Option.match(maybeUser, { onNone: () => Effect.logInfo(`Result: User with ID ${id} was not found.`), onSome: (user) => Effect.logInfo(`Result: Found user ${user.name}.`), }) ), Effect.catchAll((error) => Effect.logInfo("Error: Could not connect to the database.") ) ); // Run the program with different IDs Effect.runPromise( Effect.gen(function* () { // Try with existing user yield* Effect.logInfo("Looking for user with ID 1..."); yield* program(1); // Try with non-existent user yield* Effect.logInfo("\nLooking for user with ID 2..."); yield* program(2); }) ); ``` ## Handle API Errors **Rule:** Model application errors as typed classes and use Http.server.serveOptions to map them to specific HTTP responses. ### Example This example defines two custom error types, `UserNotFoundError` and `InvalidIdError`. The route logic can fail with either. The `unhandledErrorResponse` function inspects the error and returns a `404` or `400` response accordingly, with a generic `500` for any other unexpected errors. ```typescript import { Cause, Data, Effect } from "effect"; // Define our domain types export interface User { readonly id: string; readonly name: string; readonly email: string; readonly role: "admin" | "user"; } // Define specific, typed errors for our domain export class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ readonly id: string; }> {} export class InvalidIdError extends Data.TaggedError("InvalidIdError")<{ readonly id: string; readonly reason: string; }> {} export class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{ readonly action: string; readonly role: string; }> {} // Define error handler service export class ErrorHandlerService extends Effect.Service()( "ErrorHandlerService", { sync: () => ({ // Handle API errors with proper logging handleApiError: (error: E): Effect.Effect => Effect.gen(function* () { yield* Effect.logError(`API Error: ${JSON.stringify(error)}`); if (error instanceof UserNotFoundError) { return { error: "Not Found", message: `User ${error.id} not found`, }; } if (error instanceof InvalidIdError) { return { error: "Bad Request", message: error.reason }; } if (error instanceof UnauthorizedError) { return { error: "Unauthorized", message: `${error.role} cannot ${error.action}`, }; } return { error: "Internal Server Error", message: "An unexpected error occurred", }; }), // Handle unexpected errors handleUnexpectedError: ( cause: Cause.Cause ): Effect.Effect => Effect.gen(function* () { yield* Effect.logError("Unexpected error occurred"); if (Cause.isDie(cause)) { const defect = Cause.failureOption(cause); if (defect._tag === "Some") { const error = defect.value as Error; yield* Effect.logError(`Defect: ${error.message}`); yield* Effect.logError( `Stack: ${error.stack?.split("\n")[1]?.trim() ?? "N/A"}` ); } } return Effect.succeed(void 0); }), }), } ) {} // Define UserRepository service export class UserRepository extends Effect.Service()( "UserRepository", { sync: () => { const users = new Map([ [ "user_123", { id: "user_123", name: "Paul", email: "paul@example.com", role: "admin", }, ], [ "user_456", { id: "user_456", name: "Alice", email: "alice@example.com", role: "user", }, ], ]); return { // Get user by ID with proper error handling getUser: ( id: string ): Effect.Effect => Effect.gen(function* () { yield* Effect.logInfo(`Attempting to get user with id: ${id}`); // Validate ID format if (!id.match(/^user_\d+$/)) { yield* Effect.logWarning(`Invalid user ID format: ${id}`); return yield* Effect.fail( new InvalidIdError({ id, reason: "ID must be in format user_", }) ); } const user = users.get(id); if (user === undefined) { yield* Effect.logWarning(`User not found with id: ${id}`); return yield* Effect.fail(new UserNotFoundError({ id })); } yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); return user; }), // Check if user has required role checkRole: ( user: User, requiredRole: "admin" | "user" ): Effect.Effect => Effect.gen(function* () { yield* Effect.logInfo( `Checking if user ${user.id} has role: ${requiredRole}` ); if (user.role !== requiredRole && user.role !== "admin") { yield* Effect.logWarning( `User ${user.id} with role ${user.role} cannot access ${requiredRole} resources` ); return yield* Effect.fail( new UnauthorizedError({ action: "access_user", role: user.role, }) ); } yield* Effect.logInfo( `User ${user.id} has required role: ${user.role}` ); return Effect.succeed(void 0); }), }; }, } ) {} interface ApiResponse { readonly error?: string; readonly message?: string; readonly data?: User; } // Create routes with proper error handling const createRoutes = () => Effect.gen(function* () { const repo = yield* UserRepository; const errorHandler = yield* ErrorHandlerService; yield* Effect.logInfo("=== Processing API request ==="); // Test different scenarios for (const userId of ["user_123", "user_456", "invalid_id", "user_789"]) { yield* Effect.logInfo(`\n--- Testing user ID: ${userId} ---`); const response = yield* repo.getUser(userId).pipe( Effect.map((user) => ({ data: { ...user, email: user.role === "admin" ? user.email : "[hidden]", }, })), Effect.catchAll((error) => errorHandler.handleApiError(error)) ); yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`); } // Test role checking const adminUser = yield* repo.getUser("user_123"); const regularUser = yield* repo.getUser("user_456"); yield* Effect.logInfo("\n=== Testing role checks ==="); yield* repo.checkRole(adminUser, "admin").pipe( Effect.tap(() => Effect.logInfo("Admin access successful")), Effect.catchAll((error) => errorHandler.handleApiError(error)) ); yield* repo.checkRole(regularUser, "admin").pipe( Effect.tap(() => Effect.logInfo("User admin access successful")), Effect.catchAll((error) => errorHandler.handleApiError(error)) ); return { message: "Tests completed successfully" }; }); // Run the program with all services Effect.runPromise( Effect.provide( Effect.provide(createRoutes(), ErrorHandlerService.Default), UserRepository.Default ) ); ``` ## Handle Errors with catchTag, catchTags, and catchAll **Rule:** Handle errors with catchTag, catchTags, and catchAll. ### Example ```typescript import { Data, Effect } from "effect"; // Define domain types interface User { readonly id: string; readonly name: string; } // Define specific error types class NetworkError extends Data.TaggedError("NetworkError")<{ readonly url: string; readonly code: number; }> {} class ValidationError extends Data.TaggedError("ValidationError")<{ readonly field: string; readonly message: string; }> {} class NotFoundError extends Data.TaggedError("NotFoundError")<{ readonly id: string; }> {} // Define UserService class UserService extends Effect.Service()("UserService", { sync: () => ({ // Fetch user data fetchUser: ( id: string ): Effect.Effect => Effect.gen(function* () { yield* Effect.logInfo(`Fetching user with id: ${id}`); if (id === "invalid") { const url = "/api/users/" + id; yield* Effect.logWarning(`Network error accessing: ${url}`); return yield* Effect.fail(new NetworkError({ url, code: 500 })); } if (id === "missing") { yield* Effect.logWarning(`User not found: ${id}`); return yield* Effect.fail(new NotFoundError({ id })); } const user = { id, name: "John Doe" }; yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); return user; }), // Validate user data validateUser: (user: User): Effect.Effect => Effect.gen(function* () { yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`); if (user.name.length < 3) { yield* Effect.logWarning( `Validation failed: name too short for user ${user.id}` ); return yield* Effect.fail( new ValidationError({ field: "name", message: "Name too short" }) ); } const message = `User ${user.name} is valid`; yield* Effect.logInfo(message); return message; }), }), }) {} // Compose operations with error handling using catchTags const processUser = ( userId: string ): Effect.Effect => Effect.gen(function* () { const userService = yield* UserService; yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`); const result = yield* userService.fetchUser(userId).pipe( Effect.flatMap(userService.validateUser), // Handle different error types with specific recovery logic Effect.catchTags({ NetworkError: (e) => Effect.gen(function* () { const message = `Network error: ${e.code} for ${e.url}`; yield* Effect.logError(message); return message; }), NotFoundError: (e) => Effect.gen(function* () { const message = `User ${e.id} not found`; yield* Effect.logWarning(message); return message; }), ValidationError: (e) => Effect.gen(function* () { const message = `Invalid ${e.field}: ${e.message}`; yield* Effect.logWarning(message); return message; }), }) ); yield* Effect.logInfo(`Result: ${result}`); return result; }); // Test with different scenarios const runTests = Effect.gen(function* () { yield* Effect.logInfo("=== Starting User Processing Tests ==="); const testCases = ["valid", "invalid", "missing"]; const results = yield* Effect.forEach(testCases, (id) => processUser(id)); yield* Effect.logInfo("=== User Processing Tests Complete ==="); return results; }); // Run the program Effect.runPromise(Effect.provide(runTests, UserService.Default)); ``` **Explanation:** Use `catchTag` to handle specific error types in a type-safe, composable way. ## Handle Flaky Operations with Retries and Timeouts **Rule:** Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects. ### Example This program attempts to fetch data from a flaky API. It will retry the request up to 3 times with increasing delays if it fails. It will also give up entirely if any single attempt takes longer than 2 seconds. ```typescript import { Data, Duration, Effect, Schedule } from "effect"; // Define domain types interface ApiResponse { readonly data: string; } // Define error types class ApiError extends Data.TaggedError("ApiError")<{ readonly message: string; readonly attempt: number; }> {} class TimeoutError extends Data.TaggedError("TimeoutError")<{ readonly duration: string; readonly attempt: number; }> {} // Define API service class ApiService extends Effect.Service()("ApiService", { sync: () => ({ // Flaky API call that might fail or be slow fetchData: (): Effect.Effect => Effect.gen(function* () { const attempt = Math.floor(Math.random() * 5) + 1; yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`); if (Math.random() > 0.3) { yield* Effect.logWarning(`Attempt ${attempt}: API call failed`); return yield* Effect.fail( new ApiError({ message: "API Error", attempt, }) ); } const delay = Math.random() * 3000; yield* Effect.logInfo( `Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms` ); yield* Effect.sleep(Duration.millis(delay)); const response = { data: "some important data" }; yield* Effect.logInfo( `Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}` ); return response; }), }), }) {} // Define retry policy: exponential backoff, up to 3 retries const retryPolicy = Schedule.exponential(Duration.millis(100)).pipe( Schedule.compose(Schedule.recurs(3)), Schedule.tapInput((error: ApiError | TimeoutError) => Effect.logWarning( `Retrying after error: ${error._tag} (Attempt ${error.attempt})` ) ) ); // Create program with proper error handling const program = Effect.gen(function* () { const api = yield* ApiService; yield* Effect.logInfo("=== Starting API calls with retry and timeout ==="); // Make multiple test calls for (let i = 1; i <= 3; i++) { yield* Effect.logInfo(`\n--- Test Call ${i} ---`); const result = yield* api.fetchData().pipe( Effect.timeout(Duration.seconds(2)), Effect.catchTag("TimeoutException", () => Effect.fail(new TimeoutError({ duration: "2 seconds", attempt: i })) ), Effect.retry(retryPolicy), Effect.catchTags({ ApiError: (error) => Effect.gen(function* () { yield* Effect.logError( `All retries failed: ${error.message} (Last attempt: ${error.attempt})` ); return { data: "fallback data due to API error" } as ApiResponse; }), TimeoutError: (error) => Effect.gen(function* () { yield* Effect.logError( `All retries timed out after ${error.duration} (Last attempt: ${error.attempt})` ); return { data: "fallback data due to timeout" } as ApiResponse; }), }) ); yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`); } yield* Effect.logInfo("\n=== API calls complete ==="); }); // Run the program Effect.runPromise(Effect.provide(program, ApiService.Default)); ``` --- ## Leverage Effect's Built-in Structured Logging **Rule:** Leverage Effect's built-in structured logging. ### Example ```typescript import { Effect } from "effect"; const program = Effect.logDebug("Processing user", { userId: 123 }); // Run the program with debug logging enabled Effect.runSync( program.pipe(Effect.tap(() => Effect.log("Debug logging enabled"))) ); ``` **Explanation:** Using Effect's logging system ensures your logs are structured, filterable, and context-aware. ## Make an Outgoing HTTP Client Request **Rule:** Use the Http.client module to make outgoing requests to keep the entire operation within the Effect ecosystem. ### Example This example creates a proxy endpoint. A request to `/proxy/posts/1` on our server will trigger an outgoing request to the JSONPlaceholder API. The response is then parsed and relayed back to the original client. ```typescript import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; import * as HttpRouter from "@effect/platform/HttpRouter"; import * as HttpServer from "@effect/platform/HttpServer"; import * as HttpResponse from "@effect/platform/HttpServerResponse"; import { Console, Data, Duration, Effect, Fiber, Layer } from "effect"; class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ id: string; }> {} export class Database extends Effect.Service()("Database", { sync: () => ({ getUser: (id: string) => id === "123" ? Effect.succeed({ name: "Paul" }) : Effect.fail(new UserNotFoundError({ id })), }), }) {} const userHandler = Effect.flatMap(HttpRouter.params, (p) => Effect.flatMap(Database, (db) => db.getUser(p["userId"] ?? "")).pipe( Effect.flatMap(HttpResponse.json) ) ); const app = HttpRouter.empty.pipe( HttpRouter.get("/users/:userId", userHandler) ); const server = NodeHttpServer.layer(() => require("node:http").createServer(), { port: 3457, }); const serverLayer = HttpServer.serve(app); const mainLayer = Layer.merge(Database.Default, server); const program = Effect.gen(function* () { yield* Console.log("Server started on http://localhost:3457"); const layer = Layer.provide(serverLayer, mainLayer); // Launch server and run for a short duration to demonstrate const serverFiber = yield* Layer.launch(layer).pipe(Effect.fork); // Wait a moment for server to start yield* Effect.sleep(Duration.seconds(1)); // Simulate some server activity yield* Console.log("Server is running and ready to handle requests"); yield* Effect.sleep(Duration.seconds(2)); // Shutdown gracefully yield* Fiber.interrupt(serverFiber); yield* Console.log("Server shutdown complete"); }); NodeRuntime.runMain( Effect.provide( program, Layer.provide(serverLayer, Layer.merge(Database.Default, server)) ) as Effect.Effect ); ``` ## Manage Shared State Safely with Ref **Rule:** Use Ref to manage shared, mutable state concurrently, ensuring atomicity. ### Example This program simulates 1,000 concurrent fibers all trying to increment a shared counter. Because we use `Ref.update`, every single increment is applied atomically, and the final result is always correct. ```typescript import { Effect, Ref } from "effect"; const program = Effect.gen(function* () { // Create a new Ref with an initial value of 0 const ref = yield* Ref.make(0); // Define an effect that increments the counter by 1 const increment = Ref.update(ref, (n) => n + 1); // Create an array of 1,000 increment effects const tasks = Array.from({ length: 1000 }, () => increment); // Run all 1,000 effects concurrently yield* Effect.all(tasks, { concurrency: "unbounded" }); // Get the final value of the counter return yield* Ref.get(ref); }); // The result will always be 1000 Effect.runPromise(program).then(console.log); ``` --- ## Mapping Errors to Fit Your Domain **Rule:** Use Effect.mapError to transform errors and create clean architectural boundaries between layers. ### Example A `UserRepository` uses a `Database` service. The `Database` can fail with specific errors, but the `UserRepository` maps them to a single, generic `RepositoryError` before they are exposed to the rest of the application. ```typescript import { Effect, Data } from "effect"; // Low-level, specific errors from the database layer class ConnectionError extends Data.TaggedError("ConnectionError") {} class QueryError extends Data.TaggedError("QueryError") {} // A generic error for the repository layer class RepositoryError extends Data.TaggedError("RepositoryError")<{ readonly cause: unknown; }> {} // The inner service const dbQuery = (): Effect.Effect< { name: string }, ConnectionError | QueryError > => Effect.fail(new ConnectionError()); // The outer service uses `mapError` to create a clean boundary. // Its public signature only exposes `RepositoryError`. const findUser = (): Effect.Effect<{ name: string }, RepositoryError> => dbQuery().pipe( Effect.mapError((error) => new RepositoryError({ cause: error })) ); // Demonstrate the error mapping const program = Effect.gen(function* () { yield* Effect.logInfo("Attempting to find user..."); try { const user = yield* findUser(); yield* Effect.logInfo(`Found user: ${user.name}`); } catch (error) { yield* Effect.logInfo("This won't be reached due to Effect error handling"); } }).pipe( Effect.catchAll((error) => Effect.gen(function* () { if (error instanceof RepositoryError) { yield* Effect.logInfo(`Repository error occurred: ${error._tag}`); if ( error.cause instanceof ConnectionError || error.cause instanceof QueryError ) { yield* Effect.logInfo(`Original cause: ${error.cause._tag}`); } } else { yield* Effect.logInfo(`Unexpected error: ${error}`); } }) ) ); Effect.runPromise(program); ``` --- ## Mocking Dependencies in Tests **Rule:** Provide mock service implementations via a test-specific Layer to isolate the unit under test. ### Example We want to test a `Notifier` service that uses an `EmailClient` to send emails. In our test, we provide a mock `EmailClient` that doesn't actually send emails but just returns a success value. ```typescript import { Effect, Layer } from "effect"; // --- The Services --- interface EmailClientService { send: (address: string, body: string) => Effect.Effect; } class EmailClient extends Effect.Service()("EmailClient", { sync: () => ({ send: (address: string, body: string) => Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)), }), }) {} interface NotifierService { notifyUser: (userId: number, message: string) => Effect.Effect; } class Notifier extends Effect.Service()("Notifier", { effect: Effect.gen(function* () { const emailClient = yield* EmailClient; return { notifyUser: (userId: number, message: string) => emailClient.send(`user-${userId}@example.com`, message), }; }), dependencies: [EmailClient.Default], }) {} // Create a program that uses the Notifier service const program = Effect.gen(function* () { yield* Effect.log("Using default EmailClient implementation..."); const notifier = yield* Notifier; yield* notifier.notifyUser(123, "Your invoice is ready."); // Create mock EmailClient that logs differently yield* Effect.log("\nUsing mock EmailClient implementation..."); const mockEmailClient = Layer.succeed(EmailClient, { send: (address: string, body: string) => Effect.sync(() => Effect.log(`MOCK: Would send to ${address} with body: ${body}`) ), } as EmailClientService); // Run the same notification with mock client yield* Effect.gen(function* () { const notifier = yield* Notifier; yield* notifier.notifyUser(123, "Your invoice is ready."); }).pipe(Effect.provide(mockEmailClient)); }); // Run the program Effect.runPromise(Effect.provide(program, Notifier.Default)); ``` --- ## Model Dependencies as Services **Rule:** Model dependencies as services. ### Example ```typescript import { Effect } from "effect"; // Define Random service with production implementation as default export class Random extends Effect.Service()("Random", { // Default production implementation sync: () => ({ next: Effect.sync(() => Math.random()), }), }) {} // Example usage const program = Effect.gen(function* () { const random = yield* Random; const value = yield* random.next; return value; }); // Run with default implementation Effect.runPromise(Effect.provide(program, Random.Default)).then((value) => console.log("Random value:", value) ); ``` **Explanation:** By modeling dependencies as services, you can easily substitute mocked or deterministic implementations for testing, leading to more reliable and predictable tests. ## Model Optional Values Safely with Option **Rule:** Use Option to explicitly model values that may be absent, avoiding null or undefined. ### Example A function that looks for a user in a database is a classic use case. It might find a user, or it might not. Returning an `Option` makes this contract explicit and safe. ```typescript import { Option } from "effect"; interface User { id: number; name: string; } const users: User[] = [ { id: 1, name: "Paul" }, { id: 2, name: "Alex" }, ]; // This function safely returns an Option, not a User or null. const findUserById = (id: number): Option.Option => { const user = users.find((u) => u.id === id); return Option.fromNullable(user); // A useful helper for existing APIs }; // The caller MUST handle both cases. const greeting = (id: number): string => findUserById(id).pipe( Option.match({ onNone: () => "User not found.", onSome: (user) => `Welcome, ${user.name}!`, }) ); console.log(greeting(1)); // "Welcome, Paul!" console.log(greeting(3)); // "User not found." ``` ## Model Validated Domain Types with Brand **Rule:** Model validated domain types with Brand. ### Example ```typescript import { Brand, Option } from "effect"; type Email = string & Brand.Brand<"Email">; const makeEmail = (s: string): Option.Option => s.includes("@") ? Option.some(s as Email) : Option.none(); // A function can now trust that its input is a valid email. const sendEmail = (email: Email, body: string) => { /* ... */ }; ``` **Explanation:** Branding ensures that only validated values are used, reducing bugs and repetitive checks. ## Parse and Validate Data with Schema.decode **Rule:** Parse and validate data with Schema.decode. ### Example ```typescript import { Effect, Schema } from "effect"; interface User { name: string; } const UserSchema = Schema.Struct({ name: Schema.String, }) as Schema.Schema; const processUserInput = (input: unknown) => Effect.gen(function* () { const user = yield* Schema.decodeUnknown(UserSchema)(input); return `Welcome, ${user.name}!`; }).pipe( Effect.catchTag("ParseError", () => Effect.succeed("Invalid user data.")) ); // Demonstrate the schema parsing const program = Effect.gen(function* () { // Test with valid input const validInput = { name: "Paul" }; const validResult = yield* processUserInput(validInput); yield* Effect.logInfo(`Valid input result: ${validResult}`); // Test with invalid input const invalidInput = { age: 25 }; // Missing 'name' field const invalidResult = yield* processUserInput(invalidInput); yield* Effect.logInfo(`Invalid input result: ${invalidResult}`); // Test with completely invalid input const badInput = "not an object"; const badResult = yield* processUserInput(badInput); yield* Effect.logInfo(`Bad input result: ${badResult}`); }); Effect.runPromise(program); ``` **Explanation:** `Schema.decode` integrates parsing and validation into the Effect workflow, making error handling composable and type-safe. ## Process a Collection in Parallel with Effect.forEach **Rule:** Use Effect.forEach with the `concurrency` option to process a collection in parallel with a fixed limit. ### Example Imagine you have a list of 100 user IDs and you need to fetch the data for each one. `Effect.forEach` with a concurrency of 10 will process them in controlled parallel batches. ```typescript import { Effect } from "effect"; // Mock function to simulate fetching a user by ID const fetchUserById = (id: number) => Effect.gen(function* () { yield* Effect.logInfo(`Fetching user ${id}...`); yield* Effect.sleep("1 second"); // Simulate network delay return { id, name: `User ${id}`, email: `user${id}@example.com` }; }); const userIds = Array.from({ length: 10 }, (_, i) => i + 1); // Process the entire array, but only run 5 fetches at a time. const program = Effect.gen(function* () { yield* Effect.logInfo("Starting parallel processing..."); const startTime = Date.now(); const users = yield* Effect.forEach(userIds, fetchUserById, { concurrency: 5, // Limit to 5 concurrent operations }); const endTime = Date.now(); yield* Effect.logInfo( `Processed ${users.length} users in ${endTime - startTime}ms` ); yield* Effect.logInfo( `First few users: ${JSON.stringify(users.slice(0, 3), null, 2)}` ); return users; }); // The result will be an array of all user objects. // The total time will be much less than running them sequentially. Effect.runPromise(program); ``` --- ## Process a Large File with Constant Memory **Rule:** Use Stream.fromReadable with a Node.js Readable stream to process files efficiently. ### Example This example demonstrates reading a text file, splitting it into individual lines, and processing each line. The combination of `Stream.fromReadable`, `Stream.decodeText`, and `Stream.splitLines` is a powerful and common pattern for handling text-based files. ```typescript import { FileSystem } from "@effect/platform"; import { NodeFileSystem } from "@effect/platform-node"; import type { PlatformError } from "@effect/platform/Error"; import { Effect, Stream } from "effect"; import * as path from "node:path"; const processFile = ( filePath: string, content: string ): Effect.Effect => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; // Write content to file yield* fs.writeFileString(filePath, content); // Create a stream from file content const fileStream = Stream.fromEffect(fs.readFileString(filePath)).pipe( // Split content into lines Stream.map((content: string) => content.split("\n")), Stream.flatMap(Stream.fromIterable), // Process each line Stream.tap((line) => Effect.log(`Processing: ${line}`)) ); // Run the stream to completion yield* Stream.runDrain(fileStream); // Clean up file yield* fs.remove(filePath); }); const program = Effect.gen(function* () { const filePath = path.join(__dirname, "large-file.txt"); yield* processFile(filePath, "line 1\nline 2\nline 3").pipe( Effect.catchAll((error: PlatformError) => Effect.logError(`Error processing file: ${error.message}`) ) ); }); Effect.runPromise(program.pipe(Effect.provide(NodeFileSystem.layer))).catch( console.error ); /* Output: ... level=INFO msg="Processing: line 1" ... level=INFO msg="Processing: line 2" ... level=INFO msg="Processing: line 3" */ ``` ## Process collections of data asynchronously **Rule:** Leverage Stream to process collections effectfully with built-in concurrency control and resource safety. ### Example This example processes a list of IDs by fetching user data for each one. `Stream.mapEffect` is used to apply an effectful function (`getUserById`) to each element, with concurrency limited to 2 simultaneous requests. ```typescript import { Effect, Stream, Chunk } from "effect"; // A mock function that simulates fetching a user from a database const getUserById = ( id: number ): Effect.Effect<{ id: number; name: string }, Error> => Effect.succeed({ id, name: `User ${id}` }).pipe( Effect.delay("100 millis"), Effect.tap(() => Effect.log(`Fetched user ${id}`)) ); // The stream-based program const program = Stream.fromIterable([1, 2, 3, 4, 5]).pipe( // Process each item with an Effect, limiting concurrency to 2 Stream.mapEffect(getUserById, { concurrency: 2 }), // Run the stream and collect all results into a Chunk Stream.runCollect ); Effect.runPromise(program).then((users) => { console.log("All users fetched:", Chunk.toArray(users)); }); ``` ## Process Items Concurrently **Rule:** Use Stream.mapEffect with the `concurrency` option to process stream items in parallel. ### Example This example processes four items, each taking one second. By setting `concurrency: 2`, the total runtime is approximately two seconds instead of four, because items are processed in parallel pairs. ```typescript import { Effect, Stream } from "effect"; // A mock function that simulates a slow I/O operation const processItem = (id: number): Effect.Effect => Effect.log(`Starting item ${id}...`).pipe( Effect.delay("1 second"), Effect.map(() => `Finished item ${id}`), Effect.tap(Effect.log) ); const ids = [1, 2, 3, 4]; const program = Stream.fromIterable(ids).pipe( // Process up to 2 items concurrently Stream.mapEffect(processItem, { concurrency: 2 }), Stream.runDrain ); // Measure the total time taken const timedProgram = Effect.timed(program); Effect.runPromise(timedProgram) .then(([duration, _]) => { const durationMs = Number(duration); console.log(`\nTotal time: ${Math.round(durationMs / 1000)} seconds`); }) .catch(console.error); /* Output: ... level=INFO msg="Starting item 1..." ... level=INFO msg="Starting item 2..." ... level=INFO msg="Finished item 1" ... level=INFO msg="Starting item 3..." ... level=INFO msg="Finished item 2" ... level=INFO msg="Starting item 4..." ... level=INFO msg="Finished item 3" ... level=INFO msg="Finished item 4" Total time: 2 seconds */ ``` ## Process Items in Batches **Rule:** Use Stream.grouped(n) to transform a stream of items into a stream of batched chunks. ### Example This example processes 10 users. By using `Stream.grouped(5)`, it transforms the stream of 10 individual users into a stream of two chunks (each a batch of 5). The `saveUsersInBulk` function is then called only twice, once for each batch. ```typescript import { Effect, Stream, Chunk } from "effect"; // A mock function that simulates a bulk database insert const saveUsersInBulk = ( userBatch: Chunk.Chunk<{ id: number }> ): Effect.Effect => Effect.log( `Saving batch of ${userBatch.length} users: ${Chunk.toArray(userBatch) .map((u) => u.id) .join(", ")}` ); const userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })); const program = Stream.fromIterable(userIds).pipe( // Group the stream of users into batches of 5 Stream.grouped(5), // Process each batch with our bulk save function Stream.mapEffect(saveUsersInBulk, { concurrency: 1 }), Stream.runDrain ); Effect.runPromise(program); /* Output: ... level=INFO msg="Saving batch of 5 users: 1, 2, 3, 4, 5" ... level=INFO msg="Saving batch of 5 users: 6, 7, 8, 9, 10" */ ``` ## Process Streaming Data with Stream **Rule:** Use Stream to model and process data that arrives over time in a composable, efficient way. ### Example This example demonstrates creating a `Stream` from a paginated API. The `Stream` will make API calls as needed, processing one page of users at a time without ever holding the entire user list in memory. ```typescript import { Effect, Stream, Option } from "effect"; interface User { id: number; name: string; } interface PaginatedResponse { users: User[]; nextPage: number | null; } // A mock API call that returns a page of users const fetchUserPage = ( page: number ): Effect.Effect => Effect.succeed( page < 3 ? { users: [ { id: page * 2 + 1, name: `User ${page * 2 + 1}` }, { id: page * 2 + 2, name: `User ${page * 2 + 2}` }, ], nextPage: page + 1, } : { users: [], nextPage: null } ).pipe(Effect.delay("50 millis")); // Stream.paginateEffect creates a stream from a paginated source const userStream: Stream.Stream = Stream.paginateEffect( 0, (page) => fetchUserPage(page).pipe( Effect.map( (response) => [response.users, Option.fromNullable(response.nextPage)] as const ) ) ).pipe( // Flatten the stream of user arrays into a stream of individual users Stream.flatMap((users) => Stream.fromIterable(users)) ); // We can now process the stream of users. // Stream.runForEach will pull from the stream until it's exhausted. const program = Stream.runForEach(userStream, (user: User) => Effect.log(`Processing user: ${user.name}`) ); Effect.runPromise(program).catch(console.error); ``` --- ## Provide Configuration to Your App via a Layer **Rule:** Provide configuration to your app via a Layer. ### Example ```typescript import { Effect, Layer } from "effect"; class ServerConfig extends Effect.Service()("ServerConfig", { sync: () => ({ port: process.env.PORT ? parseInt(process.env.PORT) : 8080, }), }) {} const program = Effect.gen(function* () { const config = yield* ServerConfig; yield* Effect.log(`Starting application on port ${config.port}...`); }); Effect.runPromise(Effect.provide(program, ServerConfig.Default)).catch( console.error ); ``` **Explanation:** This approach makes configuration available contextually, supporting better testing and modularity. ## Provide Dependencies to Routes **Rule:** Define dependencies with Effect.Service and provide them to your HTTP server using a Layer. ### Example This example defines a `Database` service. The route handler for `/users/:userId` requires this service to fetch a user. We then provide a "live" implementation of the `Database` to the entire server using a `Layer`. ```typescript import * as HttpRouter from "@effect/platform/HttpRouter"; import * as HttpResponse from "@effect/platform/HttpServerResponse"; import * as HttpServer from "@effect/platform/HttpServer"; import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"; import { Effect, Duration, Fiber } from "effect/index"; import { Data } from "effect"; // 1. Define the service interface using Effect.Service export class Database extends Effect.Service()("Database", { sync: () => ({ getUser: (id: string) => id === "123" ? Effect.succeed({ name: "Paul" }) : Effect.fail(new UserNotFoundError({ id })), }), }) {} class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ id: string; }> {} // handler producing a `HttpServerResponse` const userHandler = Effect.flatMap(HttpRouter.params, (p) => Effect.flatMap(Database, (db) => db.getUser(p["userId"] ?? "")).pipe( Effect.flatMap(HttpResponse.json) ) ); // assemble router & server const app = HttpRouter.empty.pipe( HttpRouter.get("/users/:userId", userHandler) ); // Create the server effect with all dependencies const serverEffect = HttpServer.serveEffect(app).pipe( Effect.provide(Database.Default), Effect.provide( NodeHttpServer.layer(() => require("node:http").createServer(), { port: 3458, }) ) ); // Create program that manages server lifecycle const program = Effect.gen(function* () { yield* Effect.logInfo("Starting server on port 3458..."); const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.fork); yield* Effect.logInfo("Server started successfully on http://localhost:3458"); yield* Effect.logInfo("Try: curl http://localhost:3458/users/123"); yield* Effect.logInfo("Try: curl http://localhost:3458/users/456"); // Run for a short time to demonstrate yield* Effect.sleep(Duration.seconds(3)); yield* Effect.logInfo("Shutting down server..."); yield* Fiber.interrupt(serverFiber); yield* Effect.logInfo("Server shutdown complete"); }); // Run the program NodeRuntime.runMain(program); ``` ## Race Concurrent Effects for the Fastest Result **Rule:** Use Effect.race to get the result from the first of several effects to succeed, automatically interrupting the losers. ### Example A classic use case is checking a fast cache before falling back to a slower database. We can race the cache lookup against the database query. ```typescript import { Effect, Option } from "effect"; type User = { id: number; name: string }; // Simulate a slower cache lookup that might find nothing (None) const checkCache: Effect.Effect> = Effect.succeed( Option.none() ).pipe( Effect.delay("200 millis") // Made slower so database wins ); // Simulate a faster database query that will always find the data const queryDatabase: Effect.Effect> = Effect.succeed( Option.some({ id: 1, name: "Paul" }) ).pipe( Effect.delay("50 millis") // Made faster so it wins the race ); // Race them. The database should win and return the user data. const program = Effect.race(checkCache, queryDatabase).pipe( // The result of the race is an Option, so we can handle it. Effect.flatMap((result: Option.Option) => Option.match(result, { onNone: () => Effect.fail("User not found anywhere."), onSome: (user) => Effect.succeed(user), }) ) ); // In this case, the database wins the race. Effect.runPromise(program) .then((user) => { console.log("User found:", user); }) .catch((error) => { console.log("Error:", error); }); // Also demonstrate with logging const programWithLogging = Effect.gen(function* () { yield* Effect.logInfo("Starting race between cache and database..."); try { const user = yield* program; yield* Effect.logInfo( `Success: Found user ${user.name} with ID ${user.id}` ); return user; } catch (error) { yield* Effect.logInfo("This won't be reached due to Effect error handling"); return null; } }).pipe( Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.logInfo(`Handled error: ${error}`); return null; }) ) ); Effect.runPromise(programWithLogging); ``` --- ## Representing Time Spans with Duration **Rule:** Use the Duration data type to represent time intervals instead of raw numbers. ### Example This example shows how to create and use `Duration` to make time-based operations clear and unambiguous. ```typescript import { Effect, Duration } from "effect"; // Create durations with clear, explicit units const fiveSeconds = Duration.seconds(5); const oneHundredMillis = Duration.millis(100); // Use them in Effect operators const program = Effect.log("Starting...").pipe( Effect.delay(oneHundredMillis), Effect.flatMap(() => Effect.log("Running after 100ms")), Effect.timeout(fiveSeconds) // This whole operation must complete within 5 seconds ); // Durations can also be compared const isLonger = Duration.greaterThan(fiveSeconds, oneHundredMillis); // true // Demonstrate the duration functionality const demonstration = Effect.gen(function* () { yield* Effect.logInfo("=== Duration Demonstration ==="); // Show duration values yield* Effect.logInfo(`Five seconds: ${Duration.toMillis(fiveSeconds)}ms`); yield* Effect.logInfo( `One hundred millis: ${Duration.toMillis(oneHundredMillis)}ms` ); // Show comparison yield* Effect.logInfo(`Is 5 seconds longer than 100ms? ${isLonger}`); // Run the timed program yield* Effect.logInfo("Running timed program..."); yield* program; // Show more duration operations const combined = Duration.sum(fiveSeconds, oneHundredMillis); yield* Effect.logInfo(`Combined duration: ${Duration.toMillis(combined)}ms`); // Show different duration units const oneMinute = Duration.minutes(1); yield* Effect.logInfo(`One minute: ${Duration.toMillis(oneMinute)}ms`); const isMinuteLonger = Duration.greaterThan(oneMinute, fiveSeconds); yield* Effect.logInfo(`Is 1 minute longer than 5 seconds? ${isMinuteLonger}`); }); Effect.runPromise(demonstration); ``` --- ## Retry Operations Based on Specific Errors **Rule:** Use predicate-based retry policies to retry an operation only for specific, recoverable errors. ### Example This example simulates an API client that can fail with different, specific error types. The retry policy is configured to _only_ retry on `ServerBusyError` and give up immediately on `NotFoundError`. ```typescript import { Effect, Data, Schedule, Duration } from "effect"; // Define specific, tagged errors for our API client class ServerBusyError extends Data.TaggedError("ServerBusyError") {} class NotFoundError extends Data.TaggedError("NotFoundError") {} let attemptCount = 0; // A flaky API call that can fail in different ways const flakyApiCall = Effect.try({ try: () => { attemptCount++; const random = Math.random(); if (attemptCount <= 2) { // First two attempts fail with ServerBusyError (retryable) console.log( `Attempt ${attemptCount}: API call failed - Server is busy. Retrying...` ); throw new ServerBusyError(); } // Third attempt succeeds console.log(`Attempt ${attemptCount}: API call succeeded!`); return { data: "success", attempt: attemptCount }; }, catch: (e) => e as ServerBusyError | NotFoundError, }); // A predicate that returns true only for the error we want to retry const isRetryableError = (e: ServerBusyError | NotFoundError) => e._tag === "ServerBusyError"; // A policy that retries 3 times, but only if the error is retryable const selectiveRetryPolicy = Schedule.recurs(3).pipe( Schedule.whileInput(isRetryableError), Schedule.addDelay(() => "100 millis") ); const program = Effect.gen(function* () { yield* Effect.logInfo("=== Retry Based on Specific Errors Demo ==="); try { const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy)); yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`); return result; } catch (error) { yield* Effect.logInfo("This won't be reached due to Effect error handling"); return null; } }).pipe( Effect.catchAll((error) => Effect.gen(function* () { if (error instanceof NotFoundError) { yield* Effect.logInfo("Failed with NotFoundError - not retrying"); } else if (error instanceof ServerBusyError) { yield* Effect.logInfo("Failed with ServerBusyError after all retries"); } else { yield* Effect.logInfo(`Failed with unexpected error: ${error}`); } return null; }) ) ); // Also demonstrate a case where NotFoundError is not retried const demonstrateNotFound = Effect.gen(function* () { yield* Effect.logInfo("\n=== Demonstrating Non-Retryable Error ==="); const alwaysNotFound = Effect.fail(new NotFoundError()); const result = yield* alwaysNotFound.pipe( Effect.retry(selectiveRetryPolicy), Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`); return null; }) ) ); return result; }); Effect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound))); ``` --- ## Run Independent Effects in Parallel with Effect.all **Rule:** Use Effect.all to execute a collection of independent effects concurrently. ### Example Imagine fetching a user's profile and their latest posts from two different API endpoints. These are independent operations and can be run in parallel to save time. ```typescript import { Effect } from "effect"; // Simulate fetching a user, takes 1 second const fetchUser = Effect.succeed({ id: 1, name: "Paul" }).pipe( Effect.delay("1 second") ); // Simulate fetching posts, takes 1.5 seconds const fetchPosts = Effect.succeed([{ title: "Effect is great" }]).pipe( Effect.delay("1.5 seconds") ); // Run both effects concurrently const program = Effect.all([fetchUser, fetchPosts]); // The resulting effect will succeed with a tuple: [{id, name}, [{title}]] // Total execution time will be ~1.5 seconds (the duration of the longest task). Effect.runPromise(program).then(console.log); ``` --- ## Supercharge Your Editor with the Effect LSP **Rule:** Install and use the Effect LSP extension for enhanced type information and error checking in your editor. ### Example Imagine you have the following code. Without the LSP, hovering over `program` might show a complex, hard-to-read inferred type. ```typescript import { Effect } from "effect"; // Define Logger service using Effect.Service pattern class Logger extends Effect.Service()("Logger", { sync: () => ({ log: (msg: string) => Effect.sync(() => console.log(`LOG: ${msg}`)), }), }) {} const program = Effect.succeed(42).pipe( Effect.map((n) => n.toString()), Effect.flatMap((s) => Effect.log(s)), Effect.provide(Logger.Default) ); // Run the program Effect.runPromise(program); ``` With the Effect LSP installed, your editor would display a clear, readable overlay right above the `program` variable, looking something like this: ``` // (LSP Inlay Hint) // program: Effect ``` This immediately tells you that the final program returns nothing (`void`), has no expected failures (`never`), and has no remaining requirements (`never`), so it's ready to be run. --- ## Trace Operations Across Services with Spans **Rule:** Use Effect.withSpan to create custom tracing spans for important operations. ### Example This example shows a multi-step operation. Each step, and the overall operation, is wrapped in a span. This creates a parent-child hierarchy in the trace that is easy to visualize. ```typescript import { Effect, Duration } from "effect"; const validateInput = (input: unknown) => Effect.gen(function* () { yield* Effect.logInfo("Starting input validation..."); yield* Effect.sleep(Duration.millis(10)); const result = { email: "paul@example.com" }; yield* Effect.logInfo(`✅ Input validated: ${result.email}`); return result; }).pipe( // This creates a child span Effect.withSpan("validateInput") ); const saveToDatabase = (user: { email: string }) => Effect.gen(function* () { yield* Effect.logInfo(`Saving user to database: ${user.email}`); yield* Effect.sleep(Duration.millis(50)); const result = { id: 123, ...user }; yield* Effect.logInfo(`✅ User saved with ID: ${result.id}`); return result; }).pipe( // This span includes useful attributes Effect.withSpan("saveToDatabase", { attributes: { "db.system": "postgresql", "db.user.email": user.email }, }) ); const createUser = (input: unknown) => Effect.gen(function* () { yield* Effect.logInfo("=== Creating User with Tracing ==="); yield* Effect.logInfo( "This demonstrates how spans trace operations through the call stack" ); const validated = yield* validateInput(input); const user = yield* saveToDatabase(validated); yield* Effect.logInfo( `✅ User creation completed: ${JSON.stringify(user)}` ); yield* Effect.logInfo( "Note: In production, spans would be sent to a tracing system like Jaeger or Zipkin" ); return user; }).pipe( // This is the parent span for the entire operation Effect.withSpan("createUserOperation") ); // Demonstrate the tracing functionality const program = Effect.gen(function* () { yield* Effect.logInfo("=== Trace Operations with Spans Demo ==="); // Create multiple users to show tracing in action const user1 = yield* createUser({ email: "user1@example.com" }); yield* Effect.logInfo("\n--- Creating second user ---"); const user2 = yield* createUser({ email: "user2@example.com" }); yield* Effect.logInfo("\n=== Summary ==="); yield* Effect.logInfo("Created users with tracing spans:"); yield* Effect.logInfo(`User 1: ID ${user1.id}, Email: ${user1.email}`); yield* Effect.logInfo(`User 2: ID ${user2.id}, Email: ${user2.email}`); }); // When run with a tracing SDK, this will produce traces with root spans // "createUserOperation" and child spans: "validateInput" and "saveToDatabase". Effect.runPromise(program); ``` --- ## Transform Data During Validation with Schema **Rule:** Use Schema.transform to safely convert data types during the validation and parsing process. ### Example This schema parses a string but produces a `Date` object, making the final data structure much more useful. ```typescript import { Schema, Effect } from "effect"; // Define types for better type safety type RawEvent = { name: string; timestamp: string; }; type ParsedEvent = { name: string; timestamp: Date; }; // Define the schema for our event const ApiEventSchema = Schema.Struct({ name: Schema.String, timestamp: Schema.String, }); // Example input const rawInput: RawEvent = { name: "User Login", timestamp: "2025-06-22T20:08:42.000Z", }; // Parse and transform const program = Effect.gen(function* () { const parsed = yield* Schema.decode(ApiEventSchema)(rawInput); return { name: parsed.name, timestamp: new Date(parsed.timestamp), } as ParsedEvent; }); Effect.runPromise(program).then( (event) => { console.log("Event year:", event.timestamp.getFullYear()); console.log("Full event:", event); }, (error) => { console.error("Failed to parse event:", error); } ); ``` `transformOrFail` is perfect for creating branded types, as the validation can fail. ```typescript import { Schema, Effect, Brand, Either } from "effect"; type Email = string & Brand.Brand<"Email">; const Email = Schema.string.pipe( Schema.transformOrFail( Schema.brand("Email"), (s, _, ast) => s.includes("@") ? Either.right(s as Email) : Either.left(Schema.ParseError.create(ast, "Invalid email format")), (email) => Either.right(email) ) ); const result = Schema.decode(Email)("paul@example.com"); // Succeeds const errorResult = Schema.decode(Email)("invalid-email"); // Fails ``` --- ## Turn a Paginated API into a Single Stream **Rule:** Use Stream.paginateEffect to model a paginated data source as a single, continuous stream. ### Example This example simulates fetching users from a paginated API. The `fetchUsersPage` function gets one page of data and returns the next page number. `Stream.paginateEffect` uses this function to create a single stream of all users across all pages. ```typescript import { Effect, Stream, Chunk, Option } from "effect"; // --- Mock Paginated API --- interface User { id: number; name: string; } // Define FetchError as a class with a literal type tag class FetchError { readonly _tag = "FetchError" as const; constructor(readonly message: string) {} } // Helper to create FetchError instances const fetchError = (message: string): FetchError => new FetchError(message); const allUsers: User[] = Array.from({ length: 25 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}`, })); // This function simulates fetching a page of users from an API. const fetchUsersPage = ( page: number ): Effect.Effect<[Chunk.Chunk, Option.Option], FetchError> => Effect.gen(function* () { const pageSize = 10; const offset = (page - 1) * pageSize; // Simulate potential API errors if (page < 1) { return yield* Effect.fail(fetchError("Invalid page number")); } const users = Chunk.fromIterable(allUsers.slice(offset, offset + pageSize)); const nextPage = Chunk.isNonEmpty(users) && allUsers.length > offset + pageSize ? Option.some(page + 1) : Option.none(); yield* Effect.log(`Fetched page ${page}`); return [users, nextPage]; }); // --- The Pattern --- // Use paginateEffect, providing an initial state (page 1) and the fetch function. const userStream = Stream.paginateEffect(1, fetchUsersPage); const program = userStream.pipe( Stream.runCollect, Effect.map((users) => users.length), Effect.tap((totalUsers) => Effect.log(`Total users fetched: ${totalUsers}`)), Effect.catchTag("FetchError", (error) => Effect.succeed(`Error fetching users: ${error.message}`) ) ); // Run the program Effect.runPromise(program).then(console.log); /* Output: ... level=INFO msg="Fetched page 1" ... level=INFO msg="Fetched page 2" ... level=INFO msg="Fetched page 3" ... level=INFO msg="Total users fetched: 25" 25 */ ``` ## Understand Layers for Dependency Injection **Rule:** Understand that a Layer is a blueprint describing how to construct a service and its dependencies. ### Example Here, we define a `Notifier` service that requires a `Logger` to be built. The `NotifierLive` layer's type signature, `Layer`, clearly documents this dependency. ```typescript import { Effect } from "effect"; // Define the Logger service with a default implementation export class Logger extends Effect.Service()("Logger", { // Provide a synchronous implementation sync: () => ({ log: (msg: string) => Effect.sync(() => console.log(`LOG: ${msg}`)), }), }) {} // Define the Notifier service that depends on Logger export class Notifier extends Effect.Service()("Notifier", { // Provide an implementation that requires Logger effect: Effect.gen(function* () { const logger = yield* Logger; return { notify: (msg: string) => logger.log(`Notifying: ${msg}`), }; }), // Specify dependencies dependencies: [Logger.Default], }) {} // Create a program that uses both services const program = Effect.gen(function* () { const notifier = yield* Notifier; yield* notifier.notify("Hello, World!"); }); // Run the program with the default implementations Effect.runPromise(Effect.provide(program, Notifier.Default)); ``` --- ## Use Chunk for High-Performance Collections **Rule:** Prefer Chunk over Array for immutable collection operations within data processing pipelines for better performance. ### Example This example shows how to create and manipulate a `Chunk`. The API is very similar to `Array`, but the underlying performance characteristics for these immutable operations are superior. ```typescript import { Chunk } from "effect"; // Create a Chunk from an array let numbers = Chunk.fromIterable([1, 2, 3, 4, 5]); // Append a new element. This is much faster than [...arr, 6] on large collections. numbers = Chunk.append(numbers, 6); // Prepend an element. numbers = Chunk.prepend(numbers, 0); // Take the first 3 elements const firstThree = Chunk.take(numbers, 3); // Convert back to an array when you need to interface with other libraries const finalArray = Chunk.toReadonlyArray(firstThree); console.log(finalArray); // [0, 1, 2] ``` --- ## Use Effect.gen for Business Logic **Rule:** Use Effect.gen for business logic. ### Example ```typescript import { Effect } from "effect"; // Concrete implementations for demonstration const validateUser = ( data: any ): Effect.Effect<{ email: string; password: string }, Error, never> => Effect.gen(function* () { yield* Effect.logInfo(`Validating user data: ${JSON.stringify(data)}`); if (!data.email || !data.password) { return yield* Effect.fail(new Error("Email and password are required")); } if (data.password.length < 6) { return yield* Effect.fail( new Error("Password must be at least 6 characters") ); } yield* Effect.logInfo("✅ User data validated successfully"); return { email: data.email, password: data.password }; }); const hashPassword = (pw: string): Effect.Effect => Effect.gen(function* () { yield* Effect.logInfo("Hashing password..."); // Simulate password hashing const hashed = `hashed_${pw}_${Date.now()}`; yield* Effect.logInfo("✅ Password hashed successfully"); return hashed; }); const dbCreateUser = (data: { email: string; password: string; }): Effect.Effect<{ id: number; email: string }, never, never> => Effect.gen(function* () { yield* Effect.logInfo(`Creating user in database: ${data.email}`); // Simulate database operation const user = { id: Math.floor(Math.random() * 1000), email: data.email }; yield* Effect.logInfo(`✅ User created with ID: ${user.id}`); return user; }); const createUser = ( userData: any ): Effect.Effect<{ id: number; email: string }, Error, never> => Effect.gen(function* () { const validated = yield* validateUser(userData); const hashed = yield* hashPassword(validated.password); return yield* dbCreateUser({ ...validated, password: hashed }); }); // Demonstrate using Effect.gen for business logic const program = Effect.gen(function* () { yield* Effect.logInfo("=== Using Effect.gen for Business Logic Demo ==="); // Example 1: Successful user creation yield* Effect.logInfo("\n1. Creating a valid user:"); const validUser = yield* createUser({ email: "paul@example.com", password: "securepassword123", }).pipe( Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to create user: ${error.message}`); return { id: -1, email: "error" }; }) ) ); yield* Effect.logInfo(`Created user: ${JSON.stringify(validUser)}`); // Example 2: Invalid user data yield* Effect.logInfo("\n2. Attempting to create user with invalid data:"); const invalidUser = yield* createUser({ email: "invalid@example.com", password: "123", // Too short }).pipe( Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.logError(`Failed to create user: ${error.message}`); return { id: -1, email: "error" }; }) ) ); yield* Effect.logInfo(`Result: ${JSON.stringify(invalidUser)}`); yield* Effect.logInfo("\n✅ Business logic demonstration completed!"); }); Effect.runPromise(program); ``` **Explanation:** `Effect.gen` allows you to express business logic in a clear, sequential style, improving maintainability. ## Use the Auto-Generated .Default Layer in Tests **Rule:** Use the auto-generated .Default layer in tests. ### Example ```typescript import { Effect } from "effect"; // Define MyService using Effect.Service pattern class MyService extends Effect.Service()("MyService", { sync: () => ({ doSomething: () => Effect.succeed("done").pipe( Effect.tap(() => Effect.log("MyService did something!")) ), }), }) {} // Create a program that uses MyService const program = Effect.gen(function* () { yield* Effect.log("Getting MyService..."); const service = yield* MyService; yield* Effect.log("Calling doSomething()..."); const result = yield* service.doSomething(); yield* Effect.log(`Result: ${result}`); }); // Run the program with default service implementation Effect.runPromise(Effect.provide(program, MyService.Default)); ``` **Explanation:** This approach ensures your tests are idiomatic, maintainable, and take full advantage of Effect's dependency injection system. ## Validate Request Body **Rule:** Use Http.request.schemaBodyJson with a Schema to automatically parse and validate request bodies. ### Example This example defines a `POST` route to create a user. It uses a `CreateUser` schema to validate the request body. If validation passes, it returns a success message with the typed data. If it fails, the platform automatically sends a descriptive 400 error. ```typescript import { Duration, Effect } from "effect"; import * as S from "effect/Schema"; import { createServer, IncomingMessage, ServerResponse } from "http"; // Define user schema const UserSchema = S.Struct({ name: S.String, email: S.String.pipe(S.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)), }); type User = S.Schema.Type; // Define user service interface interface UserServiceInterface { readonly validateUser: (data: unknown) => Effect.Effect; } // Define user service class UserService extends Effect.Service()("UserService", { sync: () => ({ validateUser: (data: unknown) => S.decodeUnknown(UserSchema)(data), }), }) {} // Define HTTP server service interface interface HttpServerInterface { readonly handleRequest: ( request: IncomingMessage, response: ServerResponse ) => Effect.Effect; readonly start: () => Effect.Effect; } // Define HTTP server service class HttpServer extends Effect.Service()("HttpServer", { // Define effect-based implementation that uses dependencies effect: Effect.gen(function* () { const userService = yield* UserService; return { handleRequest: (request: IncomingMessage, response: ServerResponse) => Effect.gen(function* () { // Only handle POST /users if (request.method !== "POST" || request.url !== "/users") { response.writeHead(404, { "Content-Type": "application/json" }); response.end(JSON.stringify({ error: "Not Found" })); return; } try { // Read request body const body = yield* Effect.async((resume) => { let data = ""; request.on("data", (chunk) => { data += chunk; }); request.on("end", () => { try { resume(Effect.succeed(JSON.parse(data))); } catch (e) { resume( Effect.fail(e instanceof Error ? e : new Error(String(e))) ); } }); request.on("error", (e) => resume( Effect.fail(e instanceof Error ? e : new Error(String(e))) ) ); }); // Validate body against schema const user = yield* userService.validateUser(body); response.writeHead(200, { "Content-Type": "application/json" }); response.end( JSON.stringify({ message: `Successfully created user: ${user.name}`, }) ); } catch (error) { response.writeHead(400, { "Content-Type": "application/json" }); response.end(JSON.stringify({ error: String(error) })); } }), start: function (this: HttpServer) { const self = this; return Effect.gen(function* () { // Create HTTP server const server = createServer((req, res) => Effect.runFork(self.handleRequest(req, res)) ); // Add cleanup finalizer yield* Effect.addFinalizer(() => Effect.gen(function* () { yield* Effect.sync(() => server.close()); yield* Effect.logInfo("Server shut down"); }) ); // Start server yield* Effect.async((resume) => { server.on("error", (error) => resume(Effect.fail(error))); server.listen(3456, () => { Effect.runFork( Effect.logInfo("Server running at http://localhost:3456/") ); resume(Effect.succeed(void 0)); }); }); // Run for demonstration period yield* Effect.sleep(Duration.seconds(3)); yield* Effect.logInfo("Demo completed - shutting down server"); }); }, }; }), // Specify dependencies dependencies: [UserService.Default], }) {} // Create program with proper error handling const program = Effect.gen(function* () { const server = yield* HttpServer; yield* Effect.logInfo("Starting HTTP server..."); yield* server.start().pipe( Effect.catchAll((error) => Effect.gen(function* () { yield* Effect.logError(`Server error: ${error}`); return yield* Effect.fail(error); }) ) ); }).pipe( Effect.scoped // Ensure server is cleaned up ); // Run the server Effect.runFork(Effect.provide(program, HttpServer.Default)); /* To test: - POST http://localhost:3456/users with body {"name": "Paul", "email": "paul@effect.com"} -> Returns 200 OK with message "Successfully created user: Paul" - POST http://localhost:3456/users with body {"name": "Paul"} -> Returns 400 Bad Request with error message about missing email field */ ``` ## Write Tests That Adapt to Application Code **Rule:** Write tests that adapt to application code. ### Example ```typescript import { Effect } from "effect"; // Define our types interface User { id: number; name: string; } class NotFoundError extends Error { readonly _tag = "NotFoundError"; constructor(readonly id: number) { super(`User ${id} not found`); } } // Define database service interface interface DatabaseServiceApi { getUserById: (id: number) => Effect.Effect; } // Implement the service with mock data class DatabaseService extends Effect.Service()( "DatabaseService", { sync: () => ({ getUserById: (id: number) => { // Simulate database lookup if (id === 404) { return Effect.fail(new NotFoundError(id)); } return Effect.succeed({ id, name: `User ${id}` }); }, }), } ) {} // Test service implementation for testing class TestDatabaseService extends Effect.Service()( "TestDatabaseService", { sync: () => ({ getUserById: (id: number) => { // Test data with predictable responses const testUsers = [ { id: 1, name: "Test User 1" }, { id: 2, name: "Test User 2" }, { id: 123, name: "User 123" }, ]; const user = testUsers.find((u) => u.id === id); if (user) { return Effect.succeed(user); } return Effect.fail(new NotFoundError(id)); }, }), } ) {} // Business logic that uses the database service const getUserWithFallback = (id: number) => Effect.gen(function* () { const db = yield* DatabaseService; return yield* Effect.gen(function* () { const user = yield* db.getUserById(id); return user; }).pipe( Effect.catchAll((error) => Effect.gen(function* () { if (error instanceof NotFoundError) { yield* Effect.logInfo(`User ${id} not found, using fallback`); return { id, name: `Fallback User ${id}` }; } return yield* Effect.fail(error); }) ) ); }); // Create a program that demonstrates the service const program = Effect.gen(function* () { yield* Effect.logInfo( "=== Writing Tests that Adapt to Application Code Demo ===" ); const db = yield* DatabaseService; // Example 1: Successful user lookup yield* Effect.logInfo("\n1. Looking up existing user 123..."); const user = yield* Effect.gen(function* () { try { return yield* db.getUserById(123); } catch (error) { yield* Effect.logError( `Failed to get user: ${error instanceof Error ? error.message : "Unknown error"}` ); return { id: -1, name: "Error" }; } }); yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`); // Example 2: Handle non-existent user with proper error handling yield* Effect.logInfo("\n2. Looking up non-existent user 404..."); const notFoundUser = yield* Effect.gen(function* () { try { return yield* db.getUserById(404); } catch (error) { if (error instanceof NotFoundError) { yield* Effect.logInfo( `✅ Properly handled NotFoundError: ${error.message}` ); return { id: 404, name: "Not Found" }; } yield* Effect.logError( `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}` ); return { id: -1, name: "Error" }; } }); yield* Effect.logInfo(`Result: ${JSON.stringify(notFoundUser)}`); // Example 3: Business logic with fallback yield* Effect.logInfo("\n3. Business logic with fallback for missing user:"); const userWithFallback = yield* getUserWithFallback(999); yield* Effect.logInfo( `User with fallback: ${JSON.stringify(userWithFallback)}` ); // Example 4: Testing with different service implementation yield* Effect.logInfo("\n4. Testing with test service implementation:"); yield* Effect.provide( Effect.gen(function* () { const testDb = yield* TestDatabaseService; // Test existing user const testUser1 = yield* Effect.gen(function* () { try { return yield* testDb.getUserById(1); } catch (error) { yield* Effect.logError( `Test failed: ${error instanceof Error ? error.message : "Unknown error"}` ); return { id: -1, name: "Test Error" }; } }); yield* Effect.logInfo(`Test user 1: ${JSON.stringify(testUser1)}`); // Test non-existing user const testUser404 = yield* Effect.gen(function* () { try { return yield* testDb.getUserById(404); } catch (error) { yield* Effect.logInfo( `✅ Test service properly threw NotFoundError: ${error instanceof Error ? error.message : "Unknown error"}` ); return { id: 404, name: "Test Not Found" }; } }); yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`); }), TestDatabaseService.Default ); yield* Effect.logInfo( "\n✅ Tests that adapt to application code demonstration completed!" ); yield* Effect.logInfo( "The same business logic works with different service implementations!" ); }); // Run the program with the default database service Effect.runPromise( Effect.provide(program, DatabaseService.Default) as Effect.Effect< void, never, never > ); ``` **Explanation:** Tests should reflect the real interface and behavior of your code, not force changes to it.