diff --git a/app/sm-cli/package.json b/app/sm-cli/package.json index fca9cec1..535d7de8 100644 --- a/app/sm-cli/package.json +++ b/app/sm-cli/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@effect/cli": "^0.40.8", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.59.1", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", diff --git a/bun.lock b/bun.lock index c354f71f..dcda011d 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "effect-patterns-hub", "dependencies": { "@effect/cli": "^0.69.2", - "@effect/platform": "^0.93.8", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.100.0", "@langchain/openai": "^1.2.0", "@opentelemetry/api": "^1.9.0", @@ -177,7 +177,7 @@ }, "dependencies": { "@effect/cli": "^0.40.8", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.59.1", "@effect/printer": "^0.47.0", "@effect/printer-ansi": "^0.47.0", @@ -205,7 +205,7 @@ "@effect-patterns/pipeline-state": "workspace:*", "@effect-patterns/toolkit": "workspace:*", "@effect/cli": "^0.69.2", - "@effect/platform": "^0.93.8", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.98.4", "conventional-commits-parser": "^6.2.1", "conventional-recommended-bump": "^11.2.0", @@ -242,7 +242,7 @@ "name": "@effect-patterns/effect-discord", "version": "0.1.0", "dependencies": { - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", }, "devDependencies": { "typescript": "^5.5.3", @@ -310,7 +310,7 @@ "name": "@effect-patterns/toolkit", "version": "0.1.0", "dependencies": { - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "effect": "^3.18.2", @@ -322,7 +322,7 @@ "vitest": "^4.0.4", }, "peerDependencies": { - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "effect": "^3.18.2", @@ -334,7 +334,7 @@ "dependencies": { "@effect-patterns/toolkit": "workspace:*", "@effect/opentelemetry": "^0.59.2", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "@opentelemetry/api": "^1.9.0", @@ -370,7 +370,7 @@ }, "dependencies": { "@effect-patterns/toolkit": "workspace:*", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.94.2", "@effect/schema": "^0.75.5", "@modelcontextprotocol/sdk": "^1.20.0", @@ -578,7 +578,7 @@ "@effect/opentelemetry": ["@effect/opentelemetry@0.59.2", "", { "peerDependencies": { "@effect/platform": "^0.93.6", "@opentelemetry/api": "^1.9", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^3.19.9" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-yyBy+BQElgBwFOy72gCt8UHpiX0r0uPbwcC2ObLiK1Be1HfWQr0DTFdypYINiQ8QxsazfXS9HDzvV3GDjG1roQ=="], - "@effect/platform": ["@effect/platform@0.93.8", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.12" } }, "sha512-xTEy6fyTy4ijmFC3afKgtvYtn/JyPoIov4ZSUWJZUv3VeOcUPNGrrqG6IJlWkXs3NhvSywKv7wc1kw3epCQVZw=="], + "@effect/platform": ["@effect/platform@0.94.0", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.13" } }, "sha512-oIATd3M+RUe2q+bu0Qpgt4/qSbXBM9OEGBrRW7SvtwXGOWkCYkN+LfOPnmPrbMB7jYjpIb7808T1bONM1Nwgfg=="], "@effect/platform-node": ["@effect/platform-node@0.100.0", "", { "dependencies": { "@effect/platform-node-shared": "^0.53.0", "mime": "^3.0.0", "undici": "^7.10.0", "ws": "^8.18.2" }, "peerDependencies": { "@effect/cluster": "^0.52.0", "@effect/platform": "^0.93.0", "@effect/rpc": "^0.72.1", "@effect/sql": "^0.48.0", "effect": "^3.19.0" } }, "sha512-awfeW8zy5hgyRy+ml4bHK27DukTrTV5xvDhZioRF7USjJRwlZ+irdWhR6ExY+UOnAsmq0h8m/1s0l9pUcBi0bw=="], diff --git a/content/published/rules/rules-compact.md b/content/published/rules/rules-compact.md index e94843ba..6b94a418 100644 --- a/content/published/rules/rules-compact.md +++ b/content/published/rules/rules-compact.md @@ -1,228 +1,3 @@ # Effect-TS Patterns - Compact Rules -- **Access Configuration from the Context**: Access configuration from the Effect context. -- **Access Environment Variables**: Use Effect to access environment variables with proper error handling. -- **Accessing the Current Time with Clock**: Use the Clock service to get the current time, enabling deterministic testing with TestClock. -- **Accumulate Multiple Errors with Either**: Use Either to accumulate multiple validation errors instead of failing on the first one. -- **Accumulate Multiple Errors with Either**: Use Either to model computations that may fail, making errors explicit and type-safe. -- **Add Caching by Wrapping a Layer**: Use a wrapping Layer to add cross-cutting concerns like caching to a service without altering its original implementation. -- **Add Custom Metrics to Your Application**: Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring. -- **Add Custom Metrics to Your Application**: Use Effect's Metric module to define and update custom metrics for business and performance monitoring. -- **Add Rate Limiting to APIs**: Use a rate limiter service to enforce request quotas per client. -- **Add Timeouts to HTTP Requests**: Always set timeouts on HTTP requests to ensure your application doesn't hang. -- **Automatically Retry Failed Operations**: Compose a Stream with the .retry(Schedule) operator to automatically recover from transient failures. -- **Avoid Long Chains of .andThen; Use Generators Instead**: Prefer generators over long chains of .andThen. -- **Beyond the Date Type - Real World Dates, Times, and Timezones**: Use the Clock service for testable time-based logic and immutable primitives for timestamps. -- **Build a Basic HTTP Server**: Use a managed Runtime created from a Layer to handle requests in a Node.js HTTP server. -- **Cache HTTP Responses**: Use an in-memory or persistent cache to store HTTP responses. -- **Chaining Computations with flatMap**: Use flatMap to sequence computations, flattening nested structures and preserving error and context handling. -- **Checking Option and Either Cases**: Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic. -- **Collect All Results into a List**: Use Stream.runCollect to execute a stream and collect all its emitted values into a Chunk. -- **Combining Values with zip**: Use zip to run two computations and combine their results into a tuple, preserving error and context handling. -- **Comparing Data by Value with Data.struct**: Use Data.struct to define objects whose equality is based on their contents, enabling safe and predictable comparisons. -- **Comparing Data by Value with Structural Equality**: Use Data.struct or implement the Equal interface for value-based comparison of objects and classes. -- **Compose API Middleware**: Use Effect composition to build a middleware pipeline that processes requests. -- **Compose Resource Lifecycles with `Layer.merge`**: Compose multiple scoped layers using `Layer.merge` or by providing one layer to another. -- **Concurrency Pattern 1: Coordinate Async Operations with Deferred**: Use Deferred for one-time async coordination between fibers, enabling multiple consumers to wait for a single producer's result. -- **Concurrency Pattern 2: Rate Limit Concurrent Access with Semaphore**: Use Semaphore to limit concurrent access to resources, preventing overload and enabling fair resource distribution. -- **Concurrency Pattern 3: Coordinate Multiple Fibers with Latch**: Use Latch to coordinate multiple fibers awaiting a common completion signal, enabling fan-out/fan-in and barrier synchronization patterns. -- **Concurrency Pattern 4: Distribute Work with Queue**: Use Queue to distribute work between producers and consumers with built-in backpressure, enabling flexible pipeline coordination. -- **Concurrency Pattern 5: Broadcast Events with PubSub**: Use PubSub to broadcast events to multiple subscribers, enabling event-driven architectures where publishers and subscribers are loosely coupled. -- **Concurrency Pattern 6: Race and Timeout Competing Effects**: Use race to compete effects and timeout to enforce deadlines, enabling cancellation when operations exceed time limits or complete. -- **Conditional Branching with if, when, and cond**: Use combinators such as if, when, and cond to branch computations based on runtime conditions, without imperative if statements. -- **Conditionally Branching Workflows**: Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching. -- **Configure CORS for APIs**: Configure CORS headers to allow legitimate cross-origin requests while blocking unauthorized ones. -- **Configure Linting for Effect**: Use Biome for fast linting with Effect-friendly configuration. -- **Control Flow with Conditional Combinators**: Use conditional combinators for control flow. -- **Control Repetition with Schedule**: Use Schedule to create composable policies for controlling the repetition and retrying of effects. -- **Converting from Nullable, Option, or Either**: Use fromNullable, fromOption, and fromEither to lift nullable values, Option, or Either into Effects or Streams for safe, typeful interop. -- **Create a Basic HTTP Server**: Use Http.server.serve with a platform-specific layer to run an HTTP application. -- **Create a Managed Runtime for Scoped Resources**: Create a managed runtime for scoped resources. -- **Create a Reusable Runtime from Layers**: Create a reusable runtime from layers. -- **Create a Service Layer from a Managed Resource**: Provide a managed resource to the application context using `Layer.scoped`. -- **Create a Stream from a List**: Use Stream.fromIterable to begin a pipeline from an in-memory collection. -- **Create a Testable HTTP Client Service**: Define an HttpClient service with distinct Live and Test layers to enable testable API interactions. -- **Create Observability Dashboards**: Create focused dashboards that answer specific questions about system health. -- **Create Pre-resolved Effects with succeed and fail**: Create pre-resolved effects with succeed and fail. -- **Create Type-Safe Errors**: Use Data.TaggedError to create typed, distinguishable errors for your domain. -- **Creating from Collections**: Use fromIterable and fromArray to lift collections into Streams or Effects for batch or streaming processing. -- **Creating from Synchronous and Callback Code**: Use sync and async to create Effects from synchronous or callback-based computations, making them composable and type-safe. -- **Debug Effect Programs**: Use Effect.tap and logging to inspect values without changing program flow. -- **Decouple Fibers with Queues and PubSub**: Use Queue for point-to-point work distribution and PubSub for broadcast messaging between fibers. -- **Define a Type-Safe Configuration Schema**: Define a type-safe configuration schema. -- **Define Contracts Upfront with Schema**: Define contracts upfront with schema. -- **Define Type-Safe Errors with Data.TaggedError**: Define type-safe errors with Data.TaggedError. -- **Distinguish 'Not Found' from Errors**: Use Effect> to distinguish between recoverable 'not found' cases and actual failures. -- **Effectful Pattern Matching with matchEffect**: Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases. -- **Error Handling Pattern 1: Accumulating Multiple Errors**: Use error accumulation to report all problems at once rather than failing early, critical for validation and batch operations. -- **Error Handling Pattern 2: Error Propagation and Chains**: Use error propagation to preserve context through effect chains, enabling debugging and recovery at the right abstraction level. -- **Error Handling Pattern 3: Custom Error Strategies**: Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies. -- **Execute Asynchronous Effects with Effect.runPromise**: Execute asynchronous effects with Effect.runPromise. -- **Execute Long-Running Apps with Effect.runFork**: Use Effect.runFork to launch a long-running application as a manageable, detached fiber. -- **Execute Synchronous Effects with Effect.runSync**: Execute synchronous effects with Effect.runSync. -- **Export Metrics to Prometheus**: Use Effect metrics and expose a /metrics endpoint for Prometheus scraping. -- **Extract Path Parameters**: Define routes with colon-prefixed parameters (e.g., /users/:id) and access their values within the handler. -- **Fan Out to Multiple Consumers**: Use broadcast or partition to send stream data to multiple consumers. -- **Filtering Results with filter**: Use filter to declaratively express conditional logic, keeping only values that satisfy a predicate. -- **Generate OpenAPI Documentation**: Use Schema definitions to automatically generate OpenAPI documentation for your API. -- **Handle a GET Request**: Use Http.router.get to associate a URL path with a specific response Effect. -- **Handle API Errors**: Model application errors as typed classes and use Http.server.serveOptions to map them to specific HTTP responses. -- **Handle Errors with catchTag, catchTags, and catchAll**: Handle errors with catchTag, catchTags, and catchAll. -- **Handle Flaky Operations with Retries and Timeouts**: Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects. -- **Handle Missing Values with Option**: Use Option instead of null/undefined to make missing values explicit and type-safe. -- **Handle Rate Limiting Responses**: Detect 429 responses and automatically retry after the Retry-After period. -- **Handle Resource Timeouts**: Always set timeouts on resource acquisition to prevent indefinite waits. -- **Handle Unexpected Errors by Inspecting the Cause**: Handle unexpected errors by inspecting the cause. -- **Handle Unexpected Errors by Inspecting the Cause**: Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions. -- **Handle Your First Error with Effect.fail and catchAll**: Handle errors with Effect.fail and catchAll. -- **Handling Errors with catchAll, orElse, and match**: Use error handling combinators to recover from failures, provide fallback values, or transform errors in a composable way. -- **Handling Specific Errors with catchTag and catchTags**: Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic. -- **Hello World: Your First Effect**: Create your first Effect program with Effect.succeed. -- **Implement API Authentication**: Use middleware to validate authentication tokens before handling requests. -- **Implement Backpressure in Pipelines**: Use buffering and throttling to handle producers faster than consumers. -- **Implement Dead Letter Queues**: Capture failed items with context for debugging and retry instead of losing them. -- **Implement Distributed Tracing**: Propagate trace context across service boundaries to correlate requests. -- **Implement Graceful Shutdown for Your Application**: Use Effect.runFork and OS signal listeners to implement graceful shutdown for long-running applications. -- **Instrument and Observe Function Calls with Effect.fn**: Use Effect.fn to wrap functions with effectful instrumentation, such as logging, metrics, or tracing, in a composable and type-safe way. -- **Integrate Effect Tracing with OpenTelemetry**: Integrate Effect.withSpan with OpenTelemetry to export traces and visualize request flows across services. -- **Leverage Effect's Built-in Structured Logging**: Use Effect.log, Effect.logInfo, and Effect.logError to add structured, context-aware logging to your Effect code. -- **Leverage Effect's Built-in Structured Logging**: Leverage Effect's built-in structured logging. -- **Lifting Errors and Absence with fail, none, and left**: Use fail, none, and left to create Effect, Option, or Either that represent failure or absence. -- **Lifting Values with succeed, some, and right**: Use succeed, some, and right to create Effect, Option, or Either from plain values. -- **Log HTTP Requests and Responses**: Use Effect's logging to trace HTTP requests for debugging and monitoring. -- **Make an Outgoing HTTP Client Request**: Use the Http.client module to make outgoing requests to keep the entire operation within the Effect ecosystem. -- **Manage Hierarchical Resources**: Use nested Scopes to manage resources with parent-child dependencies. -- **Manage Resource Lifecycles with Scope**: Use Scope for fine-grained, manual control over resource lifecycles and cleanup guarantees. -- **Manage Resources Safely in a Pipeline**: Use Stream.acquireRelease to safely manage the lifecycle of a resource within a pipeline. -- **Manage Shared State Safely with Ref**: Use Ref to manage shared, mutable state concurrently, ensuring atomicity. -- **Manage Shared State Safely with Ref**: Use Ref to safely manage shared, mutable state in concurrent and effectful programs. -- **Manually Manage Lifecycles with `Scope`**: Use `Effect.scope` and `Scope.addFinalizer` for fine-grained control over resource cleanup. -- **Mapping and Chaining over Collections with forEach and all**: Use forEach and all to process collections of values with effectful functions, collecting results in a type-safe and composable way. -- **Mapping Errors to Fit Your Domain**: Use Effect.mapError to transform errors and create clean architectural boundaries between layers. -- **Matching on Success and Failure with match**: Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively. -- **Matching Tagged Unions with matchTag and matchTags**: Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way. -- **Merge Multiple Streams**: Use merge, concat, or zip to combine multiple streams based on your requirements. -- **Mocking Dependencies in Tests**: Provide mock service implementations via a test-specific Layer to isolate the unit under test. -- **Model Dependencies as Services**: Model dependencies as services. -- **Model Optional Values Safely with Option**: Use Option to explicitly model values that may be absent, avoiding null or undefined. -- **Model Optional Values Safely with Option**: Use Option to model values that may be present or absent, making absence explicit and type-safe. -- **Model Validated Domain Types with Brand**: Model validated domain types with Brand. -- **Modeling Effect Results with Exit**: Use Exit to capture the outcome of an Effect, including success, failure, and defects, for robust error handling and coordination. -- **Modeling Tagged Unions with Data.case**: Use Data.case to define tagged unions (ADTs) for modeling domain-specific states and enabling exhaustive pattern matching. -- **Modeling Validated Domain Types with Brand**: Use Brand to define types like Email, UserId, or PositiveInt, ensuring only valid values can be constructed and used. -- **Optional Pattern 1: Handling None and Some Values**: Use Option to represent values that may not exist, replacing null/undefined with type-safe Option that forces explicit handling. -- **Optional Pattern 2: Optional Chaining and Composition**: Use Option combinators (map, flatMap, ap) to compose operations that may fail, creating readable and maintainable pipelines. -- **Organize Layers into Composable Modules**: Organize services into modular Layers that are composed hierarchically to manage complexity in large applications. -- **Parse and Validate Data with Schema.decode**: Parse and validate data with Schema.decode. -- **Parse JSON Responses Safely**: Always validate HTTP responses with Schema to catch API changes at runtime. -- **Pattern Match on Option and Either**: Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values -- **Platform Pattern 1: Execute Shell Commands**: Use Command to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling. -- **Platform Pattern 2: Filesystem Operations**: Use FileSystem module for safe, resource-managed file operations with proper error handling and cleanup. -- **Platform Pattern 3: Persistent Key-Value Storage**: Use KeyValueStore for simple persistent storage of key-value pairs, enabling lightweight caching and session management. -- **Platform Pattern 4: Interactive Terminal I/O**: Use Terminal for user input/output in CLI applications, providing proper buffering and cross-platform character encoding. -- **Platform Pattern 5: Cross-Platform Path Manipulation**: Use Effect's platform-aware path utilities to handle separators, absolute/relative paths, and environment variables consistently. -- **Platform Pattern 6: Advanced FileSystem Operations**: Use advanced file system patterns to implement efficient, reliable file operations with proper error handling and resource cleanup. -- **Poll for Status Until a Task Completes**: Use Effect.race to run a repeating polling task that is automatically interrupted when a main task completes. -- **Pool Resources for Reuse**: Use Pool to manage expensive resources that can be reused across operations. -- **Process a Collection in Parallel with Effect.forEach**: Use Effect.forEach with the `concurrency` option to process a collection in parallel with a fixed limit. -- **Process a Large File with Constant Memory**: Use Stream.fromReadable with a Node.js Readable stream to process files efficiently. -- **Process collections of data asynchronously**: Leverage Stream to process collections effectfully with built-in concurrency control and resource safety. -- **Process Items Concurrently**: Use Stream.mapEffect with the `concurrency` option to process stream items in parallel. -- **Process Items in Batches**: Use Stream.grouped(n) to transform a stream of items into a stream of batched chunks. -- **Process Streaming Data with Stream**: Use Stream to model and process data that arrives over time in a composable, efficient way. -- **Profile Effect Applications**: Use Effect's timing features and Node.js profilers to find performance bottlenecks. -- **Property-Based Testing with Effect**: Use property-based testing to find edge cases your example-based tests miss. -- **Provide Configuration to Your App via a Layer**: Provide configuration to your app via a Layer. -- **Provide Dependencies to Routes**: Define dependencies with Effect.Service and provide them to your HTTP server using a Layer. -- **Race Concurrent Effects for the Fastest Result**: Use Effect.race to get the result from the first of several effects to succeed, automatically interrupting the losers. -- **Race Effects and Handle Timeouts**: Use Effect.race for fastest-wins, Effect.timeout for time limits. -- **Read Effect Type Errors**: Effect errors are verbose but structured - learn to extract the key information. -- **Redact and Handle Sensitive Data**: Use Redacted to wrap sensitive values, preventing accidental exposure in logs or error messages. -- **Representing Time Spans with Duration**: Use Duration to model and manipulate time spans, enabling safe and expressive time-based logic. -- **Representing Time Spans with Duration**: Use the Duration data type to represent time intervals instead of raw numbers. -- **Retry a Failed Operation with Effect.retry**: Retry failed operations with Effect.retry. -- **Retry Failed Operations**: Use Effect.retry with a Schedule to handle transient failures gracefully. -- **Retry HTTP Requests with Backoff**: Use Schedule to retry failed HTTP requests with configurable backoff strategies. -- **Retry Operations Based on Specific Errors**: Use predicate-based retry policies to retry an operation only for specific, recoverable errors. -- **Run a Pipeline for its Side Effects**: Use Stream.runDrain to execute a stream for its side effects when you don't need the final values. -- **Run Background Tasks with Effect.fork**: Use Effect.fork to start a non-blocking background process and manage its lifecycle via its Fiber. -- **Run Independent Effects in Parallel with Effect.all**: Use Effect.all to execute a collection of independent effects concurrently. -- **Run Multiple Effects in Parallel with Effect.all**: Run multiple Effects in parallel with Effect.all. -- **Running and Collecting Stream Results**: Choose the right Stream.run* method based on what you need from the results. -- **Safely Bracket Resource Usage with `acquireRelease`**: Bracket the use of a resource between an `acquire` and a `release` effect. -- **Scheduling Pattern 1: Repeat an Effect on a Fixed Interval**: Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks. -- **Scheduling Pattern 2: Implement Exponential Backoff for Retries**: Use exponential backoff with jitter for retries to prevent overwhelming failing services and improve success likelihood through smart timing. -- **Scheduling Pattern 3: Schedule Tasks with Cron Expressions**: Use cron expressions to schedule periodic tasks at specific calendar times, enabling flexible scheduling beyond simple fixed intervals. -- **Scheduling Pattern 4: Debounce and Throttle Execution**: Use debounce to wait for silence before executing, and throttle to limit execution frequency, both critical for handling rapid events. -- **Scheduling Pattern 5: Advanced Retry Chains and Circuit Breakers**: Use retry chains with circuit breakers to handle complex failure scenarios, detect cascade failures early, and prevent resource exhaustion. -- **Send a JSON Response**: Use Http.response.json to automatically serialize data structures into a JSON response. -- **Sequencing with andThen, tap, and flatten**: Use sequencing combinators to run computations in order, perform side effects, or flatten nested structures, while preserving error and context handling. -- **Set Up a New Effect Project**: Set up a new Effect project. -- **Set Up Alerting**: Create alerts based on SLOs and symptoms, not causes. -- **Set Up CI/CD for Effect Projects**: Use GitHub Actions with proper caching for fast Effect project CI/CD. -- **Set Up Your Effect Development Environment**: Install the Effect extension and configure TypeScript for optimal Effect development. -- **Sink Pattern 1: Batch Insert Stream Records into Database**: Batch stream records before database operations to improve throughput and reduce transaction overhead. -- **Sink Pattern 2: Write Stream Events to Event Log**: Append stream events to an event log with metadata to maintain a complete, ordered record of what happened. -- **Sink Pattern 3: Write Stream Lines to File**: Write streaming lines to a file efficiently using buffered output and proper resource management. -- **Sink Pattern 4: Send Stream Records to Message Queue**: Stream records to message queues with proper batching and acknowledgment for reliable distributed data flow. -- **Sink Pattern 5: Fall Back to Alternative Sink on Failure**: Implement fallback sinks to handle failures gracefully and ensure data is persisted even when the primary destination is unavailable. -- **Sink Pattern 6: Retry Failed Stream Operations**: Implement retry strategies in sinks to handle transient failures and improve resilience without manual intervention. -- **Solve Promise Problems with Effect**: Recognize that Effect solves the core limitations of Promises: untyped errors, no dependency injection, and no cancellation. -- **State Management Pattern 1: Synchronized Reference with SynchronizedRef**: Use SynchronizedRef for thread-safe mutable state that must be updated consistently across concurrent operations, with atomic modifications. -- **State Management Pattern 2: Observable State with SubscriptionRef**: Combine Ref with PubSub to create observable state where changes trigger notifications, enabling reactive state management. -- **Stream Pattern 1: Transform Streams with Map and Filter**: Use map and filter combinators to transform stream elements declaratively, creating pipelines that reshape data without materializing intermediate results. -- **Stream Pattern 2: Merge and Combine Multiple Streams**: Use merge and concat combinators to combine multiple streams, enabling aggregation of data from multiple independent sources. -- **Stream Pattern 3: Control Backpressure in Streams**: Use backpressure control to manage flow between fast producers and slow consumers, preventing memory exhaustion and resource overflow. -- **Stream Pattern 4: Stateful Operations with Scan and Fold**: Use scan for stateful element-by-element processing and fold for final aggregation, enabling complex stream analytics without buffering entire stream. -- **Stream Pattern 5: Grouping and Windowing Streams**: Use groupBy to partition streams by key and tumbling/sliding windows to aggregate streams over time windows. -- **Stream Pattern 6: Resource Management in Streams**: Use Stream.bracket or effect scoping to guarantee resource cleanup, preventing leaks even when streams fail or are interrupted. -- **Stream Pattern 7: Error Handling in Streams**: Use Stream error handlers to recover from failures, retry operations, and maintain stream integrity even when individual elements fail. -- **Stream Pattern 8: Advanced Stream Transformations**: Use advanced stream operators to build sophisticated data pipelines that compose elegantly and maintain performance at scale. -- **Stream vs Effect - When to Use Which**: Use Effect for single values, Stream for sequences of values. -- **Supercharge Your Editor with the Effect LSP**: Install and use the Effect LSP extension for enhanced type information and error checking in your editor. -- **Take and Drop Stream Elements**: Use take/drop to control stream size, takeWhile/dropWhile for conditional limits. -- **Teach your AI Agents Effect with the MCP Server**: Use the MCP server to provide live application context to AI coding agents, enabling more accurate assistance. -- **Test Concurrent Code**: Use TestClock and controlled concurrency to make concurrent tests deterministic. -- **Test Effects with Services**: Provide test implementations of services to make Effect programs testable. -- **Test Streaming Effects**: Use Stream.runCollect and assertions to verify stream behavior. -- **Trace Operations Across Services with Spans**: Use Effect.withSpan to create and annotate tracing spans for operations, enabling distributed tracing and performance analysis. -- **Trace Operations Across Services with Spans**: Use Effect.withSpan to create custom tracing spans for important operations. -- **Transform Data During Validation with Schema**: Use Schema.transform to safely convert data types during the validation and parsing process. -- **Transform Effect Values with map and flatMap**: Transform Effect values with map and flatMap. -- **Transform Values with Effect.map**: Transform Effect values with map. -- **Transforming Values with map**: Use map to apply a pure function to the value inside an Effect, Stream, Option, or Either. -- **Turn a Paginated API into a Single Stream**: Use Stream.paginateEffect to model a paginated data source as a single, continuous stream. -- **Type Classes for Equality, Ordering, and Hashing with Data.Class**: Use Data.Class to define and derive type classes for your data types, supporting composable equality, ordering, and hashing. -- **Understand Fibers as Lightweight Threads**: Understand that a Fiber is a lightweight, virtual thread managed by the Effect runtime for massive concurrency. -- **Understand Layers for Dependency Injection**: Understand that a Layer is a blueprint describing how to construct a service and its dependencies. -- **Understand that Effects are Lazy Blueprints**: Understand that effects are lazy blueprints. -- **Understand the Three Effect Channels (A, E, R)**: Understand that an Effect<A, E, R> describes a computation with a success type (A), an error type (E), and a requirements type (R). -- **Understanding Fibers**: Fibers are lightweight threads managed by Effect, enabling efficient concurrency without OS thread overhead. -- **Use .pipe for Composition**: Use .pipe for composition. -- **Use Chunk for High-Performance Collections**: Use Chunk to model immutable, high-performance collections for efficient data processing and transformation. -- **Use Chunk for High-Performance Collections**: Prefer Chunk over Array for immutable collection operations within data processing pipelines for better performance. -- **Use Effect DevTools**: Use Effect's built-in debugging features and logging for development. -- **Use Effect.gen for Business Logic**: Use Effect.gen for business logic. -- **Use the Auto-Generated .Default Layer in Tests**: Use the auto-generated .Default layer in tests. -- **Validate Request Body**: Use Http.request.schemaBodyJson with a Schema to automatically parse and validate request bodies. -- **Validating and Parsing Branded Types**: Combine Schema and Brand to validate and parse branded types, guaranteeing only valid domain values are created at runtime. -- **Why Effect? Comparing Effect to Promise**: Understand why Effect is better than raw Promises. -- **Work with Arbitrary-Precision Numbers using BigDecimal**: Use BigDecimal to represent and compute with decimal numbers that require arbitrary precision, such as in finance or scientific domains. -- **Work with Dates and Times using DateTime**: Use DateTime to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way. -- **Work with Immutable Sets using HashSet**: Use HashSet to represent sets of unique values with efficient, immutable operations for membership, union, intersection, and difference. -- **Working with Immutable Arrays using Data.array**: Use Data.array to define arrays whose equality is based on their contents, enabling safe, predictable comparisons and functional operations. -- **Working with Tuples using Data.tuple**: Use Data.tuple to define tuples whose equality is based on their contents, enabling safe and predictable comparisons and pattern matching. -- **Wrap Asynchronous Computations with tryPromise**: Wrap asynchronous computations with tryPromise. -- **Wrap Synchronous Computations with sync and try**: Wrap synchronous computations with sync and try. -- **Wrapping Synchronous and Asynchronous Computations**: Use try and tryPromise to lift code that may throw or reject into Effect, capturing errors in the failure channel. -- **Write Sequential Code with Effect.gen**: Write sequential code with Effect.gen. -- **Write Tests That Adapt to Application Code**: Write tests that adapt to application code. -- **Your First Domain Model**: Start domain modeling by defining clear interfaces for your business entities. -- **Your First Effect Test**: Use Effect.runPromise in tests to run and assert on Effect results. -- **Your First Error Handler**: Use catchAll or catchTag to recover from errors and keep your program running. -- **Your First HTTP Request**: Use @effect/platform HttpClient for type-safe HTTP requests with automatic error handling. -- **Your First Logs**: Use Effect.log and related functions for structured, contextual logging. -- **Your First Parallel Operation**: Use Effect.all with concurrency option to run independent effects in parallel. -- **Your First Platform Operation**: Use @effect/platform for cross-platform system operations with Effect integration. -- **Your First Schedule**: Use Schedule to control when and how often effects run. -- **Your First Stream**: Use Stream to process sequences of data lazily and efficiently. +- **Test Rule**: A test rule description diff --git a/content/published/rules/rules.json b/content/published/rules/rules.json index e777aeea..acbce02f 100644 --- a/content/published/rules/rules.json +++ b/content/published/rules/rules.json @@ -1,2940 +1,13 @@ [ { - "id": "access-config-in-context", - "title": "Access Configuration from the Context", - "description": "Access configuration from the Effect context.", - "skillLevel": "intermediate", + "id": "test-1", + "title": "Test Rule", + "description": "A test rule description", + "skillLevel": "Beginner", "useCases": [ - "core-concepts" + "Testing" ], - "example": "```typescript\nimport { Config, Effect, Layer } from \"effect\";\n\n// Define config service\nclass AppConfig extends Effect.Service()(\"AppConfig\", {\n sync: () => ({\n host: \"localhost\",\n port: 3000,\n }),\n}) {}\n\n// Create program that uses config\nconst program = Effect.gen(function* () {\n const config = yield* AppConfig;\n yield* Effect.log(`Starting server on http://${config.host}:${config.port}`);\n});\n\n// Run the program with default config\nEffect.runPromise(Effect.provide(program, AppConfig.Default));\n```\n\n**Explanation:** \nBy yielding the config object, you make your dependency explicit and leverage Effect's context system for testability and modularity.", - "antiPattern": "Passing configuration values down through multiple function arguments (\"prop-drilling\"). This is cumbersome and obscures which components truly need which values.", - "explanation": "This allows your business logic to declaratively state its dependency on a piece of configuration. The logic is clean, type-safe, and completely decoupled from _how_ the configuration is provided.", - "content": "# Access Configuration from the Context\n\n## Guideline\n\nInside an `Effect.gen` block, use `yield*` on your `Config` object to access the resolved, type-safe configuration values from the context.\n\n## Rationale\n\nThis allows your business logic to declaratively state its dependency on a piece of configuration. The logic is clean, type-safe, and completely decoupled from _how_ the configuration is provided.\n\n## Good Example\n\n```typescript\nimport { Config, Effect, Layer } from \"effect\";\n\n// Define config service\nclass AppConfig extends Effect.Service()(\"AppConfig\", {\n sync: () => ({\n host: \"localhost\",\n port: 3000,\n }),\n}) {}\n\n// Create program that uses config\nconst program = Effect.gen(function* () {\n const config = yield* AppConfig;\n yield* Effect.log(`Starting server on http://${config.host}:${config.port}`);\n});\n\n// Run the program with default config\nEffect.runPromise(Effect.provide(program, AppConfig.Default));\n```\n\n**Explanation:** \nBy yielding the config object, you make your dependency explicit and leverage Effect's context system for testability and modularity.\n\n## Anti-Pattern\n\nPassing configuration values down through multiple function arguments (\"prop-drilling\"). This is cumbersome and obscures which components truly need which values." - }, - { - "id": "platform-environment-variables", - "title": "Access Environment Variables", - "description": "Use Effect to access environment variables with proper error handling.", - "skillLevel": "beginner", - "useCases": [ - "platform-getting-started" - ], - "example": "```typescript\nimport { Effect, Config, Option } from \"effect\"\n\n// ============================================\n// BASIC: Read required variable\n// ============================================\n\nconst getApiKey = Config.string(\"API_KEY\")\n\nconst program1 = Effect.gen(function* () {\n const apiKey = yield* getApiKey\n yield* Effect.log(`API Key: ${apiKey.slice(0, 4)}...`)\n})\n\n// ============================================\n// OPTIONAL: With default value\n// ============================================\n\nconst getPort = Config.number(\"PORT\").pipe(\n Config.withDefault(3000)\n)\n\nconst program2 = Effect.gen(function* () {\n const port = yield* getPort\n yield* Effect.log(`Server will run on port ${port}`)\n})\n\n// ============================================\n// OPTIONAL: Return Option instead of failing\n// ============================================\n\nconst getOptionalFeature = Config.string(\"FEATURE_FLAG\").pipe(\n Config.option\n)\n\nconst program3 = Effect.gen(function* () {\n const feature = yield* getOptionalFeature\n \n if (Option.isSome(feature)) {\n yield* Effect.log(`Feature enabled: ${feature.value}`)\n } else {\n yield* Effect.log(\"Feature flag not set\")\n }\n})\n\n// ============================================\n// COMBINED: Multiple variables as config object\n// ============================================\n\nconst AppConfig = Config.all({\n apiKey: Config.string(\"API_KEY\"),\n apiUrl: Config.string(\"API_URL\"),\n port: Config.number(\"PORT\").pipe(Config.withDefault(3000)),\n debug: Config.boolean(\"DEBUG\").pipe(Config.withDefault(false)),\n})\n\nconst program4 = Effect.gen(function* () {\n const config = yield* AppConfig\n \n yield* Effect.log(`API URL: ${config.apiUrl}`)\n yield* Effect.log(`Port: ${config.port}`)\n yield* Effect.log(`Debug: ${config.debug}`)\n})\n\n// ============================================\n// RUN: Will fail if required vars missing\n// ============================================\n\nEffect.runPromise(program4).catch((error) => {\n console.error(\"Missing required environment variables\")\n console.error(error)\n})\n```", - "antiPattern": "", - "explanation": "Environment variables can be missing or malformed. Effect helps you:\n\n1. **Handle missing vars** - Return `Option` or fail with typed error\n2. **Validate values** - Parse and validate with Schema\n3. **Provide defaults** - Fallback values when vars are missing\n4. **Document requirements** - Types show what's needed\n\n---", - "content": "## Guideline\n\nAccess environment variables using Effect's built-in functions or Platform's environment service for type-safe configuration.\n\n---\n\n## Rationale\n\nEnvironment variables can be missing or malformed. Effect helps you:\n\n1. **Handle missing vars** - Return `Option` or fail with typed error\n2. **Validate values** - Parse and validate with Schema\n3. **Provide defaults** - Fallback values when vars are missing\n4. **Document requirements** - Types show what's needed\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Config, Option } from \"effect\"\n\n// ============================================\n// BASIC: Read required variable\n// ============================================\n\nconst getApiKey = Config.string(\"API_KEY\")\n\nconst program1 = Effect.gen(function* () {\n const apiKey = yield* getApiKey\n yield* Effect.log(`API Key: ${apiKey.slice(0, 4)}...`)\n})\n\n// ============================================\n// OPTIONAL: With default value\n// ============================================\n\nconst getPort = Config.number(\"PORT\").pipe(\n Config.withDefault(3000)\n)\n\nconst program2 = Effect.gen(function* () {\n const port = yield* getPort\n yield* Effect.log(`Server will run on port ${port}`)\n})\n\n// ============================================\n// OPTIONAL: Return Option instead of failing\n// ============================================\n\nconst getOptionalFeature = Config.string(\"FEATURE_FLAG\").pipe(\n Config.option\n)\n\nconst program3 = Effect.gen(function* () {\n const feature = yield* getOptionalFeature\n \n if (Option.isSome(feature)) {\n yield* Effect.log(`Feature enabled: ${feature.value}`)\n } else {\n yield* Effect.log(\"Feature flag not set\")\n }\n})\n\n// ============================================\n// COMBINED: Multiple variables as config object\n// ============================================\n\nconst AppConfig = Config.all({\n apiKey: Config.string(\"API_KEY\"),\n apiUrl: Config.string(\"API_URL\"),\n port: Config.number(\"PORT\").pipe(Config.withDefault(3000)),\n debug: Config.boolean(\"DEBUG\").pipe(Config.withDefault(false)),\n})\n\nconst program4 = Effect.gen(function* () {\n const config = yield* AppConfig\n \n yield* Effect.log(`API URL: ${config.apiUrl}`)\n yield* Effect.log(`Port: ${config.port}`)\n yield* Effect.log(`Debug: ${config.debug}`)\n})\n\n// ============================================\n// RUN: Will fail if required vars missing\n// ============================================\n\nEffect.runPromise(program4).catch((error) => {\n console.error(\"Missing required environment variables\")\n console.error(error)\n})\n```\n\n## Config Types\n\n| Method | Returns | Behavior |\n|--------|---------|----------|\n| `Config.string(name)` | `Config` | Fails if missing |\n| `Config.number(name)` | `Config` | Fails if missing or NaN |\n| `Config.boolean(name)` | `Config` | Parses \"true\"/\"false\" |\n| `Config.withDefault(value)` | `Config` | Use default if missing |\n| `Config.option` | `Config>` | None if missing |\n| `Config.all({...})` | `Config<{...}>` | Combine multiple |\n\n## Best Practices\n\n1. **Fail fast** - Use required configs for critical values\n2. **Provide defaults** - Use `withDefault` for optional settings\n3. **Validate early** - Check config at startup, not runtime\n4. **Document** - Types show what environment vars are needed" - }, - { - "id": "accessing-current-time-with-clock", - "title": "Accessing the Current Time with Clock", - "description": "Use the Clock service to get the current time, enabling deterministic testing with TestClock.", - "skillLevel": "intermediate", - "useCases": [ - "testing" - ], - "example": "This example shows a function that checks if a token is expired. Its logic depends on `Clock`, making it fully testable.\n\n```typescript\nimport { Effect, Clock, Duration } from \"effect\";\n\ninterface Token {\n readonly value: string;\n readonly expiresAt: number; // UTC milliseconds\n}\n\n// This function is pure and testable because it depends on Clock\nconst isTokenExpired = (\n token: Token\n): Effect.Effect =>\n Clock.currentTimeMillis.pipe(\n Effect.map((now) => now > token.expiresAt),\n Effect.tap((expired) =>\n Clock.currentTimeMillis.pipe(\n Effect.flatMap((currentTime) =>\n Effect.log(\n `Token expired? ${expired} (current time: ${new Date(currentTime).toISOString()})`\n )\n )\n )\n )\n );\n\n// Create a test clock service that advances time\nconst makeTestClock = (timeMs: number): Clock.Clock => ({\n currentTimeMillis: Effect.succeed(timeMs),\n currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)),\n sleep: (duration: Duration.Duration) => Effect.succeed(void 0),\n unsafeCurrentTimeMillis: () => timeMs,\n unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000),\n [Clock.ClockTypeId]: Clock.ClockTypeId,\n});\n\n// Create a token that expires in 1 second\nconst token = { value: \"abc\", expiresAt: Date.now() + 1000 };\n\n// Check token expiry with different clocks\nconst program = Effect.gen(function* () {\n // Check with current time\n yield* Effect.log(\"Checking with current time...\");\n yield* isTokenExpired(token);\n\n // Check with past time\n yield* Effect.log(\"\\nChecking with past time (1 minute ago)...\");\n const pastClock = makeTestClock(Date.now() - 60_000);\n yield* isTokenExpired(token).pipe(\n Effect.provideService(Clock.Clock, pastClock)\n );\n\n // Check with future time\n yield* Effect.log(\"\\nChecking with future time (1 hour ahead)...\");\n const futureClock = makeTestClock(Date.now() + 3600_000);\n yield* isTokenExpired(token).pipe(\n Effect.provideService(Clock.Clock, futureClock)\n );\n});\n\n// Run the program with default clock\nEffect.runPromise(\n program.pipe(Effect.provideService(Clock.Clock, makeTestClock(Date.now())))\n);\n```\n\n---", - "antiPattern": "Directly calling `Date.now()` inside your business logic. This creates an impure function that cannot be tested reliably without manipulating the system clock, which is a bad practice.\n\n```typescript\nimport { Effect } from \"effect\";\n\ninterface Token {\n readonly expiresAt: number;\n}\n\n// ❌ WRONG: This function's behavior changes every millisecond.\nconst isTokenExpiredUnsafely = (token: Token): Effect.Effect =>\n Effect.sync(() => Date.now() > token.expiresAt);\n\n// Testing this function would require complex mocking of global APIs\n// or would be non-deterministic.\n```", - "explanation": "Directly calling `Date.now()` makes your code impure and tightly coupled to the system clock. This makes testing difficult and unreliable, as the output of your function will change every time it's run.\n\nThe `Clock` service is Effect's solution to this problem. It's an abstraction for \"the current time.\"\n\n- In **production**, the default `Live` `Clock` implementation uses the real system time.\n- In **tests**, you can provide the `TestClock` layer. This gives you a virtual clock that you can manually control, allowing you to set the time to a specific value or advance it by a specific duration.\n\nThis makes any time-dependent logic pure, deterministic, and easy to test with perfect precision.\n\n---", - "content": "## Guideline\n\nWhenever you need to get the current time within an `Effect`, do not call `Date.now()` directly. Instead, depend on the `Clock` service and use one of its methods, such as `Clock.currentTimeMillis`.\n\n---\n\n## Rationale\n\nDirectly calling `Date.now()` makes your code impure and tightly coupled to the system clock. This makes testing difficult and unreliable, as the output of your function will change every time it's run.\n\nThe `Clock` service is Effect's solution to this problem. It's an abstraction for \"the current time.\"\n\n- In **production**, the default `Live` `Clock` implementation uses the real system time.\n- In **tests**, you can provide the `TestClock` layer. This gives you a virtual clock that you can manually control, allowing you to set the time to a specific value or advance it by a specific duration.\n\nThis makes any time-dependent logic pure, deterministic, and easy to test with perfect precision.\n\n---\n\n## Good Example\n\nThis example shows a function that checks if a token is expired. Its logic depends on `Clock`, making it fully testable.\n\n```typescript\nimport { Effect, Clock, Duration } from \"effect\";\n\ninterface Token {\n readonly value: string;\n readonly expiresAt: number; // UTC milliseconds\n}\n\n// This function is pure and testable because it depends on Clock\nconst isTokenExpired = (\n token: Token\n): Effect.Effect =>\n Clock.currentTimeMillis.pipe(\n Effect.map((now) => now > token.expiresAt),\n Effect.tap((expired) =>\n Clock.currentTimeMillis.pipe(\n Effect.flatMap((currentTime) =>\n Effect.log(\n `Token expired? ${expired} (current time: ${new Date(currentTime).toISOString()})`\n )\n )\n )\n )\n );\n\n// Create a test clock service that advances time\nconst makeTestClock = (timeMs: number): Clock.Clock => ({\n currentTimeMillis: Effect.succeed(timeMs),\n currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)),\n sleep: (duration: Duration.Duration) => Effect.succeed(void 0),\n unsafeCurrentTimeMillis: () => timeMs,\n unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000),\n [Clock.ClockTypeId]: Clock.ClockTypeId,\n});\n\n// Create a token that expires in 1 second\nconst token = { value: \"abc\", expiresAt: Date.now() + 1000 };\n\n// Check token expiry with different clocks\nconst program = Effect.gen(function* () {\n // Check with current time\n yield* Effect.log(\"Checking with current time...\");\n yield* isTokenExpired(token);\n\n // Check with past time\n yield* Effect.log(\"\\nChecking with past time (1 minute ago)...\");\n const pastClock = makeTestClock(Date.now() - 60_000);\n yield* isTokenExpired(token).pipe(\n Effect.provideService(Clock.Clock, pastClock)\n );\n\n // Check with future time\n yield* Effect.log(\"\\nChecking with future time (1 hour ahead)...\");\n const futureClock = makeTestClock(Date.now() + 3600_000);\n yield* isTokenExpired(token).pipe(\n Effect.provideService(Clock.Clock, futureClock)\n );\n});\n\n// Run the program with default clock\nEffect.runPromise(\n program.pipe(Effect.provideService(Clock.Clock, makeTestClock(Date.now())))\n);\n```\n\n---\n\n## Anti-Pattern\n\nDirectly calling `Date.now()` inside your business logic. This creates an impure function that cannot be tested reliably without manipulating the system clock, which is a bad practice.\n\n```typescript\nimport { Effect } from \"effect\";\n\ninterface Token {\n readonly expiresAt: number;\n}\n\n// ❌ WRONG: This function's behavior changes every millisecond.\nconst isTokenExpiredUnsafely = (token: Token): Effect.Effect =>\n Effect.sync(() => Date.now() > token.expiresAt);\n\n// Testing this function would require complex mocking of global APIs\n// or would be non-deterministic.\n```" - }, - { - "id": "accumulate-multiple-errors-with-either", - "title": "Accumulate Multiple Errors with Either", - "description": "Use Either to accumulate multiple validation errors instead of failing on the first one.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "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.\n\n```typescript\nimport { Effect, Schema, Data, Either } from \"effect\";\n\n// Define validation error type\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\n// Define user type\ntype User = {\n name: string;\n email: string;\n};\n\n// Define schema with custom validation\nconst UserSchema = Schema.Struct({\n name: Schema.String.pipe(\n Schema.minLength(3),\n Schema.filter((name) => /^[A-Za-z\\s]+$/.test(name), {\n message: () => \"name must contain only letters and spaces\",\n })\n ),\n email: Schema.String.pipe(\n Schema.pattern(/@/),\n Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, {\n message: () => \"email must be a valid email address\",\n })\n ),\n});\n\n// Example inputs\nconst invalidInputs: User[] = [\n {\n name: \"Al\", // Too short\n email: \"bob-no-at-sign.com\", // Invalid pattern\n },\n {\n name: \"John123\", // Contains numbers\n email: \"john@incomplete\", // Invalid email\n },\n {\n name: \"Alice Smith\", // Valid\n email: \"alice@example.com\", // Valid\n },\n];\n\n// Validate a single user\nconst validateUser = (input: User) =>\n Effect.gen(function* () {\n const result = yield* Schema.decode(UserSchema)(input, { errors: \"all\" });\n return result;\n });\n\n// Process multiple users and accumulate all errors\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Validating users...\\n\");\n\n for (const input of invalidInputs) {\n const result = yield* Effect.either(validateUser(input));\n\n yield* Effect.log(`Validating user: ${input.name} <${input.email}>`);\n\n // Handle success and failure cases separately for clarity\n // Using Either.match which is the idiomatic way to handle Either values\n yield* Either.match(result, {\n onLeft: (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\"❌ Validation failed:\");\n yield* Effect.log(error.message);\n yield* Effect.log(\"\"); // Empty line for readability\n }),\n onRight: (user) =>\n Effect.gen(function* () {\n yield* Effect.log(`✅ User is valid: ${JSON.stringify(user)}`);\n yield* Effect.log(\"\"); // Empty line for readability\n }),\n });\n }\n});\n\n// Run the program\nEffect.runSync(program);\n```\n\n---", - "antiPattern": "Using `Effect`'s error channel for validation that requires multiple error messages. The code below will only ever report the first error it finds, because `Effect.fail` short-circuits the entire `Effect.gen` block.\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst validateWithEffect = (input: { name: string; email: string }) =>\n Effect.gen(function* () {\n if (input.name.length < 3) {\n // The program will fail here and never check the email.\n return yield* Effect.fail(\"Name is too short.\");\n }\n if (!input.email.includes(\"@\")) {\n return yield* Effect.fail(\"Email is invalid.\");\n }\n return yield* Effect.succeed(input);\n });\n```", - "explanation": "The `Effect` error channel is designed to short-circuit. The moment an `Effect` fails, the entire computation stops and the error is propagated. This is perfect for handling unrecoverable errors like a lost database connection.\n\nHowever, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once.\n\n`Either` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Either`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation.\n\n---", - "content": "## Guideline\n\nWhen you need to perform multiple validation checks and collect all failures, use the `Either` data type. `Either` represents a value that can be one of two possibilities: a `Left` (typically for failure) or a `Right` (typically for success).\n\n---\n\n## Rationale\n\nThe `Effect` error channel is designed to short-circuit. The moment an `Effect` fails, the entire computation stops and the error is propagated. This is perfect for handling unrecoverable errors like a lost database connection.\n\nHowever, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once.\n\n`Either` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Either`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation.\n\n---\n\n## Good Example\n\nUsing `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.\n\n```typescript\nimport { Effect, Schema, Data, Either } from \"effect\";\n\n// Define validation error type\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\n// Define user type\ntype User = {\n name: string;\n email: string;\n};\n\n// Define schema with custom validation\nconst UserSchema = Schema.Struct({\n name: Schema.String.pipe(\n Schema.minLength(3),\n Schema.filter((name) => /^[A-Za-z\\s]+$/.test(name), {\n message: () => \"name must contain only letters and spaces\",\n })\n ),\n email: Schema.String.pipe(\n Schema.pattern(/@/),\n Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, {\n message: () => \"email must be a valid email address\",\n })\n ),\n});\n\n// Example inputs\nconst invalidInputs: User[] = [\n {\n name: \"Al\", // Too short\n email: \"bob-no-at-sign.com\", // Invalid pattern\n },\n {\n name: \"John123\", // Contains numbers\n email: \"john@incomplete\", // Invalid email\n },\n {\n name: \"Alice Smith\", // Valid\n email: \"alice@example.com\", // Valid\n },\n];\n\n// Validate a single user\nconst validateUser = (input: User) =>\n Effect.gen(function* () {\n const result = yield* Schema.decode(UserSchema)(input, { errors: \"all\" });\n return result;\n });\n\n// Process multiple users and accumulate all errors\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Validating users...\\n\");\n\n for (const input of invalidInputs) {\n const result = yield* Effect.either(validateUser(input));\n\n yield* Effect.log(`Validating user: ${input.name} <${input.email}>`);\n\n // Handle success and failure cases separately for clarity\n // Using Either.match which is the idiomatic way to handle Either values\n yield* Either.match(result, {\n onLeft: (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\"❌ Validation failed:\");\n yield* Effect.log(error.message);\n yield* Effect.log(\"\"); // Empty line for readability\n }),\n onRight: (user) =>\n Effect.gen(function* () {\n yield* Effect.log(`✅ User is valid: ${JSON.stringify(user)}`);\n yield* Effect.log(\"\"); // Empty line for readability\n }),\n });\n }\n});\n\n// Run the program\nEffect.runSync(program);\n```\n\n---\n\n## Anti-Pattern\n\nUsing `Effect`'s error channel for validation that requires multiple error messages. The code below will only ever report the first error it finds, because `Effect.fail` short-circuits the entire `Effect.gen` block.\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst validateWithEffect = (input: { name: string; email: string }) =>\n Effect.gen(function* () {\n if (input.name.length < 3) {\n // The program will fail here and never check the email.\n return yield* Effect.fail(\"Name is too short.\");\n }\n if (!input.email.includes(\"@\")) {\n return yield* Effect.fail(\"Email is invalid.\");\n }\n return yield* Effect.succeed(input);\n });\n```" - }, - { - "id": "data-either", - "title": "Accumulate Multiple Errors with Either", - "description": "Use Either to model computations that may fail, making errors explicit and type-safe.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Either } from \"effect\";\n\n// Create a Right (success) or Left (failure)\nconst success = Either.right(42); // Either\nconst failure = Either.left(\"Something went wrong\"); // Either\n\n// Pattern match on Either\nconst result = success.pipe(\n Either.match({\n onLeft: (err) => `Error: ${err}`,\n onRight: (value) => `Value: ${value}`,\n })\n); // string\n\n// Combine multiple Eithers and accumulate errors\nconst e1 = Either.right(1);\nconst e2 = Either.left(\"fail1\");\nconst e3 = Either.left(\"fail2\");\n\nconst all = [e1, e2, e3].filter(Either.isRight).map(Either.getRight); // [1]\nconst errors = [e1, e2, e3].filter(Either.isLeft).map(Either.getLeft); // [\"fail1\", \"fail2\"]\n```\n\n**Explanation:**\n\n- `Either.right(value)` represents success.\n- `Either.left(error)` represents failure.\n- Pattern matching ensures all cases are handled.\n- You can accumulate errors or results from multiple Eithers.", - "antiPattern": "Throwing exceptions or using ad-hoc error codes, which are not type-safe, not composable, and make error handling less predictable.", - "explanation": "`Either` is a foundational data type for error handling in functional programming. \nIt allows you to accumulate errors, model domain-specific failures, and avoid exceptions and unchecked errors.", - "content": "# Accumulate Multiple Errors with `Either`\n\n## Guideline\n\nUse the `Either` data type to represent computations that can fail (`Left`) or succeed (`Right`). \nThis makes error handling explicit, type-safe, and composable.\n\n## Rationale\n\n`Either` is a foundational data type for error handling in functional programming. \nIt allows you to accumulate errors, model domain-specific failures, and avoid exceptions and unchecked errors.\n\n## Good Example\n\n```typescript\nimport { Either } from \"effect\";\n\n// Create a Right (success) or Left (failure)\nconst success = Either.right(42); // Either\nconst failure = Either.left(\"Something went wrong\"); // Either\n\n// Pattern match on Either\nconst result = success.pipe(\n Either.match({\n onLeft: (err) => `Error: ${err}`,\n onRight: (value) => `Value: ${value}`,\n })\n); // string\n\n// Combine multiple Eithers and accumulate errors\nconst e1 = Either.right(1);\nconst e2 = Either.left(\"fail1\");\nconst e3 = Either.left(\"fail2\");\n\nconst all = [e1, e2, e3].filter(Either.isRight).map(Either.getRight); // [1]\nconst errors = [e1, e2, e3].filter(Either.isLeft).map(Either.getLeft); // [\"fail1\", \"fail2\"]\n```\n\n**Explanation:**\n\n- `Either.right(value)` represents success.\n- `Either.left(error)` represents failure.\n- Pattern matching ensures all cases are handled.\n- You can accumulate errors or results from multiple Eithers.\n\n## Anti-Pattern\n\nThrowing exceptions or using ad-hoc error codes, which are not type-safe, not composable, and make error handling less predictable." - }, - { - "id": "add-caching-by-wrapping-a-layer", - "title": "Add Caching by Wrapping a Layer", - "description": "Use a wrapping Layer to add cross-cutting concerns like caching to a service without altering its original implementation.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "We have a `WeatherService` that makes slow API calls. We create a `WeatherService.cached` wrapper layer that adds an in-memory cache using a `Ref` and a `Map`.\n\n```typescript\nimport { Effect, Layer, Ref } from \"effect\";\n\n// 1. Define the service interface\nclass WeatherService extends Effect.Service()(\n \"WeatherService\",\n {\n sync: () => ({\n getForecast: (city: string) => Effect.succeed(`Sunny in ${city}`),\n }),\n }\n) {}\n\n// 2. The \"Live\" implementation that is slow\nconst WeatherServiceLive = Layer.succeed(\n WeatherService,\n WeatherService.of({\n _tag: \"WeatherService\",\n getForecast: (city) =>\n Effect.succeed(`Sunny in ${city}`).pipe(\n Effect.delay(\"2 seconds\"),\n Effect.tap(() => Effect.log(`Fetched live forecast for ${city}`))\n ),\n })\n);\n\n// 3. The Caching Wrapper Layer\nconst WeatherServiceCached = Layer.effect(\n WeatherService,\n Effect.gen(function* () {\n // It REQUIRES the original WeatherService\n const underlyingService = yield* WeatherService;\n const cache = yield* Ref.make(new Map());\n\n return WeatherService.of({\n _tag: \"WeatherService\",\n getForecast: (city) =>\n Ref.get(cache).pipe(\n Effect.flatMap((map) =>\n map.has(city)\n ? Effect.log(`Cache HIT for ${city}`).pipe(\n Effect.as(map.get(city)!)\n )\n : Effect.log(`Cache MISS for ${city}`).pipe(\n Effect.flatMap(() => underlyingService.getForecast(city)),\n Effect.tap((forecast) =>\n Ref.update(cache, (map) => map.set(city, forecast))\n )\n )\n )\n ),\n });\n })\n);\n\n// 4. Compose the final layer. The wrapper is provided with the live implementation.\nconst AppLayer = Layer.provide(WeatherServiceCached, WeatherServiceLive);\n\n// 5. The application logic\nconst program = Effect.gen(function* () {\n const weather = yield* WeatherService;\n yield* weather.getForecast(\"London\"); // First call is slow (MISS)\n yield* weather.getForecast(\"London\"); // Second call is instant (HIT)\n});\n\nEffect.runPromise(Effect.provide(program, AppLayer));\n```\n\n---", - "antiPattern": "Modifying the original service implementation to include caching logic directly. This violates the Single Responsibility Principle by mixing the core logic of fetching weather with the cross-cutting concern of caching.\n\n```typescript\n// ❌ WRONG: The service is now responsible for both its logic and its caching strategy.\nconst WeatherServiceWithInlineCache = Layer.effect(\n WeatherService,\n Effect.gen(function* () {\n const cache = yield* Ref.make(new Map());\n return WeatherService.of({\n getForecast: (city) => {\n // ...caching logic mixed directly with fetching logic...\n return Effect.succeed(\"...\");\n },\n });\n })\n);\n```", - "explanation": "You often want to add functionality like caching, logging, or metrics to a service without polluting its core business logic. The wrapper layer pattern is a clean way to achieve this.\n\nBy creating a layer that _requires_ the original service, you can get an instance of it from the context, and then provide a _new_ implementation of that same service that calls the original.\n\nThis approach is powerful because:\n\n- **It's Non-Invasive:** The original service (`DatabaseLive`) remains completely unchanged.\n- **It's Composable:** You can apply multiple wrappers. You could wrap a database layer with a caching layer, then wrap that with a metrics layer.\n- **It's Explicit:** The composition is clearly defined at the application's top level where you build your final `AppLayer`.\n\n---", - "content": "## Guideline\n\nTo add cross-cutting concerns like caching to a service, create a \"wrapper\" `Layer`. This is a layer that takes the original service's `Layer` as input (as a dependency) and returns a new `Layer`. The new layer provides the same service interface but wraps the original methods with additional logic (e.g., checking a cache before calling the original method).\n\n---\n\n## Rationale\n\nYou often want to add functionality like caching, logging, or metrics to a service without polluting its core business logic. The wrapper layer pattern is a clean way to achieve this.\n\nBy creating a layer that _requires_ the original service, you can get an instance of it from the context, and then provide a _new_ implementation of that same service that calls the original.\n\nThis approach is powerful because:\n\n- **It's Non-Invasive:** The original service (`DatabaseLive`) remains completely unchanged.\n- **It's Composable:** You can apply multiple wrappers. You could wrap a database layer with a caching layer, then wrap that with a metrics layer.\n- **It's Explicit:** The composition is clearly defined at the application's top level where you build your final `AppLayer`.\n\n---\n\n## Good Example\n\nWe have a `WeatherService` that makes slow API calls. We create a `WeatherService.cached` wrapper layer that adds an in-memory cache using a `Ref` and a `Map`.\n\n```typescript\nimport { Effect, Layer, Ref } from \"effect\";\n\n// 1. Define the service interface\nclass WeatherService extends Effect.Service()(\n \"WeatherService\",\n {\n sync: () => ({\n getForecast: (city: string) => Effect.succeed(`Sunny in ${city}`),\n }),\n }\n) {}\n\n// 2. The \"Live\" implementation that is slow\nconst WeatherServiceLive = Layer.succeed(\n WeatherService,\n WeatherService.of({\n _tag: \"WeatherService\",\n getForecast: (city) =>\n Effect.succeed(`Sunny in ${city}`).pipe(\n Effect.delay(\"2 seconds\"),\n Effect.tap(() => Effect.log(`Fetched live forecast for ${city}`))\n ),\n })\n);\n\n// 3. The Caching Wrapper Layer\nconst WeatherServiceCached = Layer.effect(\n WeatherService,\n Effect.gen(function* () {\n // It REQUIRES the original WeatherService\n const underlyingService = yield* WeatherService;\n const cache = yield* Ref.make(new Map());\n\n return WeatherService.of({\n _tag: \"WeatherService\",\n getForecast: (city) =>\n Ref.get(cache).pipe(\n Effect.flatMap((map) =>\n map.has(city)\n ? Effect.log(`Cache HIT for ${city}`).pipe(\n Effect.as(map.get(city)!)\n )\n : Effect.log(`Cache MISS for ${city}`).pipe(\n Effect.flatMap(() => underlyingService.getForecast(city)),\n Effect.tap((forecast) =>\n Ref.update(cache, (map) => map.set(city, forecast))\n )\n )\n )\n ),\n });\n })\n);\n\n// 4. Compose the final layer. The wrapper is provided with the live implementation.\nconst AppLayer = Layer.provide(WeatherServiceCached, WeatherServiceLive);\n\n// 5. The application logic\nconst program = Effect.gen(function* () {\n const weather = yield* WeatherService;\n yield* weather.getForecast(\"London\"); // First call is slow (MISS)\n yield* weather.getForecast(\"London\"); // Second call is instant (HIT)\n});\n\nEffect.runPromise(Effect.provide(program, AppLayer));\n```\n\n---\n\n## Anti-Pattern\n\nModifying the original service implementation to include caching logic directly. This violates the Single Responsibility Principle by mixing the core logic of fetching weather with the cross-cutting concern of caching.\n\n```typescript\n// ❌ WRONG: The service is now responsible for both its logic and its caching strategy.\nconst WeatherServiceWithInlineCache = Layer.effect(\n WeatherService,\n Effect.gen(function* () {\n const cache = yield* Ref.make(new Map());\n return WeatherService.of({\n getForecast: (city) => {\n // ...caching logic mixed directly with fetching logic...\n return Effect.succeed(\"...\");\n },\n });\n })\n);\n```" - }, - { - "id": "add-custom-metrics", - "title": "Add Custom Metrics to Your Application", - "description": "Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "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.\n\n```typescript\nimport { Effect, Metric, Duration } from \"effect\"; // We don't need MetricBoundaries anymore\n\n// 1. Define your metrics\nconst userRegisteredCounter = Metric.counter(\"users_registered_total\", {\n description: \"A counter for how many users have been registered.\",\n});\n\nconst dbDurationTimer = Metric.timer(\n \"db_operation_duration\",\n \"A timer for DB operation durations\"\n);\n\n// 2. Simulated database call\nconst saveUserToDb = Effect.succeed(\"user saved\").pipe(\n Effect.delay(Duration.millis(Math.random() * 100))\n);\n\n// 3. Instrument the business logic\nconst createUser = Effect.gen(function* () {\n // Time the operation\n yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer));\n\n // Increment the counter\n yield* Metric.increment(userRegisteredCounter);\n\n return { status: \"success\" };\n});\n\n// Run the Effect\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* createUser;\n yield* Effect.log(`Result: ${JSON.stringify(result)}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "Not adding any metrics to your application. Without metrics, you are flying blind. You have no high-level overview of your application's health, performance, or business KPIs. You can't build dashboards, you can't set up alerts for abnormal behavior (e.g., \"error rate is too high\"), and you are forced to rely on digging through logs to\nunderstand the state of your system.", - "explanation": "While logs are for events and traces are for requests, metrics are for aggregation. They provide a high-level, numerical view of your system's health over time, which is perfect for building dashboards and setting up alerts.\n\nEffect's `Metric` module provides a simple, declarative way to add this instrumentation. By defining your metrics upfront, you can then use operators like `Metric.increment` or `Effect.timed` to update them. This is fully integrated with Effect's context system, allowing you to provide different metric backends (like Prometheus or StatsD) via a `Layer`.\n\nThis allows you to answer questions like:\n\n- \"What is our user sign-up rate over the last 24 hours?\"\n- \"Are we approaching our maximum number of database connections?\"\n- \"What is the 95th percentile latency for our API requests?\"\n\n---", - "content": "## Guideline\n\nTo monitor the health and performance of your application, instrument your code with `Metric`s. The three main types are:\n\n- **`Metric.counter(\"name\")`**: To count occurrences of an event (e.g., `users_registered_total`). It only goes up.\n- **`Metric.gauge(\"name\")`**: To track a value that can go up or down (e.g., `active_connections`).\n- **`Metric.histogram(\"name\")`**: To track the distribution of a value (e.g., `request_duration_seconds`).\n\n---\n\n## Rationale\n\nWhile logs are for events and traces are for requests, metrics are for aggregation. They provide a high-level, numerical view of your system's health over time, which is perfect for building dashboards and setting up alerts.\n\nEffect's `Metric` module provides a simple, declarative way to add this instrumentation. By defining your metrics upfront, you can then use operators like `Metric.increment` or `Effect.timed` to update them. This is fully integrated with Effect's context system, allowing you to provide different metric backends (like Prometheus or StatsD) via a `Layer`.\n\nThis allows you to answer questions like:\n\n- \"What is our user sign-up rate over the last 24 hours?\"\n- \"Are we approaching our maximum number of database connections?\"\n- \"What is the 95th percentile latency for our API requests?\"\n\n---\n\n## Good Example\n\nThis example creates a counter to track how many times a user is created and a histogram to track the duration of the database operation.\n\n```typescript\nimport { Effect, Metric, Duration } from \"effect\"; // We don't need MetricBoundaries anymore\n\n// 1. Define your metrics\nconst userRegisteredCounter = Metric.counter(\"users_registered_total\", {\n description: \"A counter for how many users have been registered.\",\n});\n\nconst dbDurationTimer = Metric.timer(\n \"db_operation_duration\",\n \"A timer for DB operation durations\"\n);\n\n// 2. Simulated database call\nconst saveUserToDb = Effect.succeed(\"user saved\").pipe(\n Effect.delay(Duration.millis(Math.random() * 100))\n);\n\n// 3. Instrument the business logic\nconst createUser = Effect.gen(function* () {\n // Time the operation\n yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer));\n\n // Increment the counter\n yield* Metric.increment(userRegisteredCounter);\n\n return { status: \"success\" };\n});\n\n// Run the Effect\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* createUser;\n yield* Effect.log(`Result: ${JSON.stringify(result)}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nNot adding any metrics to your application. Without metrics, you are flying blind. You have no high-level overview of your application's health, performance, or business KPIs. You can't build dashboards, you can't set up alerts for abnormal behavior (e.g., \"error rate is too high\"), and you are forced to rely on digging through logs to\nunderstand the state of your system." - }, - { - "id": "observability-custom-metrics", - "title": "Add Custom Metrics to Your Application", - "description": "Use Effect's Metric module to define and update custom metrics for business and performance monitoring.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, Metric } from \"effect\";\n\n// Define a counter metric for processed jobs\nconst jobsProcessed = Metric.counter(\"jobs_processed\");\n\n// Increment the counter when a job is processed\nconst processJob = Effect.gen(function* () {\n // ... process the job\n yield* Effect.log(\"Job processed\");\n yield* Metric.increment(jobsProcessed);\n});\n\n// Define a gauge for current active users\nconst activeUsers = Metric.gauge(\"active_users\");\n\n// Update the gauge when users sign in or out\nconst userSignedIn = Metric.set(activeUsers, 1); // Set to 1 (simplified example)\nconst userSignedOut = Metric.set(activeUsers, 0); // Set to 0 (simplified example)\n\n// Define a histogram for request durations\nconst requestDuration = Metric.histogram(\"request_duration\", [\n 0.1, 0.5, 1, 2, 5,\n] as any); // boundaries in seconds\n\n// Record a request duration\nconst recordDuration = (duration: number) =>\n Metric.update(requestDuration, duration);\n```\n\n**Explanation:**\n\n- `Metric.counter` tracks counts of events.\n- `Metric.gauge` tracks a value that can go up or down (e.g., active users).\n- `Metric.histogram` tracks distributions (e.g., request durations).\n- `Effect.updateMetric` updates the metric in your workflow.", - "antiPattern": "Relying solely on logs for monitoring, or using ad-hoc counters and variables that are not integrated with your observability stack.", - "explanation": "Metrics provide quantitative insight into your application's behavior and performance. \nBy instrumenting your code with metrics, you can monitor key events, detect anomalies, and drive business decisions.", - "content": "# Add Custom Metrics to Your Application\n\n## Guideline\n\nUse Effect's `Metric` module to define and update custom metrics such as counters, gauges, and histograms. \nThis allows you to track business events, performance indicators, and system health in a type-safe and composable way.\n\n## Rationale\n\nMetrics provide quantitative insight into your application's behavior and performance. \nBy instrumenting your code with metrics, you can monitor key events, detect anomalies, and drive business decisions.\n\n## Good Example\n\n```typescript\nimport { Effect, Metric } from \"effect\";\n\n// Define a counter metric for processed jobs\nconst jobsProcessed = Metric.counter(\"jobs_processed\");\n\n// Increment the counter when a job is processed\nconst processJob = Effect.gen(function* () {\n // ... process the job\n yield* Effect.log(\"Job processed\");\n yield* Metric.increment(jobsProcessed);\n});\n\n// Define a gauge for current active users\nconst activeUsers = Metric.gauge(\"active_users\");\n\n// Update the gauge when users sign in or out\nconst userSignedIn = Metric.set(activeUsers, 1); // Set to 1 (simplified example)\nconst userSignedOut = Metric.set(activeUsers, 0); // Set to 0 (simplified example)\n\n// Define a histogram for request durations\nconst requestDuration = Metric.histogram(\"request_duration\", [\n 0.1, 0.5, 1, 2, 5,\n] as any); // boundaries in seconds\n\n// Record a request duration\nconst recordDuration = (duration: number) =>\n Metric.update(requestDuration, duration);\n```\n\n**Explanation:**\n\n- `Metric.counter` tracks counts of events.\n- `Metric.gauge` tracks a value that can go up or down (e.g., active users).\n- `Metric.histogram` tracks distributions (e.g., request durations).\n- `Effect.updateMetric` updates the metric in your workflow.\n\n## Anti-Pattern\n\nRelying solely on logs for monitoring, or using ad-hoc counters and variables that are not integrated with your observability stack." - }, - { - "id": "api-rate-limiting", - "title": "Add Rate Limiting to APIs", - "description": "Use a rate limiter service to enforce request quotas per client.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "example": "```typescript\nimport { Effect, Context, Layer, Ref, HashMap, Data, Duration } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define rate limit types\n// ============================================\n\ninterface RateLimitConfig {\n readonly maxRequests: number\n readonly windowMs: number\n}\n\ninterface RateLimitState {\n readonly count: number\n readonly resetAt: number\n}\n\nclass RateLimitExceededError extends Data.TaggedError(\"RateLimitExceededError\")<{\n readonly retryAfter: number\n readonly limit: number\n}> {}\n\n// ============================================\n// 2. Rate limiter service\n// ============================================\n\ninterface RateLimiter {\n readonly check: (key: string) => Effect.Effect\n readonly getStatus: (key: string) => Effect.Effect<{\n remaining: number\n resetAt: number\n }>\n}\n\nclass RateLimiterService extends Context.Tag(\"RateLimiter\")<\n RateLimiterService,\n RateLimiter\n>() {}\n\n// ============================================\n// 3. In-memory rate limiter implementation\n// ============================================\n\nconst makeRateLimiter = (config: RateLimitConfig) =>\n Effect.gen(function* () {\n const state = yield* Ref.make(HashMap.empty())\n\n const getOrCreateState = (key: string, now: number) =>\n Ref.modify(state, (map) => {\n const existing = HashMap.get(map, key)\n\n if (existing._tag === \"Some\") {\n // Check if window expired\n if (now >= existing.value.resetAt) {\n // Start new window\n const newState: RateLimitState = {\n count: 0,\n resetAt: now + config.windowMs,\n }\n return [newState, HashMap.set(map, key, newState)]\n }\n return [existing.value, map]\n }\n\n // Create new entry\n const newState: RateLimitState = {\n count: 0,\n resetAt: now + config.windowMs,\n }\n return [newState, HashMap.set(map, key, newState)]\n })\n\n const incrementCount = (key: string) =>\n Ref.modify(state, (map) => {\n const existing = HashMap.get(map, key)\n if (existing._tag === \"Some\") {\n const updated = { ...existing.value, count: existing.value.count + 1 }\n return [updated.count, HashMap.set(map, key, updated)]\n }\n return [1, map]\n })\n\n const limiter: RateLimiter = {\n check: (key) =>\n Effect.gen(function* () {\n const now = Date.now()\n const currentState = yield* getOrCreateState(key, now)\n\n if (currentState.count >= config.maxRequests) {\n const retryAfter = Math.ceil((currentState.resetAt - now) / 1000)\n return yield* Effect.fail(\n new RateLimitExceededError({\n retryAfter,\n limit: config.maxRequests,\n })\n )\n }\n\n yield* incrementCount(key)\n }),\n\n getStatus: (key) =>\n Effect.gen(function* () {\n const now = Date.now()\n const currentState = yield* getOrCreateState(key, now)\n return {\n remaining: Math.max(0, config.maxRequests - currentState.count),\n resetAt: currentState.resetAt,\n }\n }),\n }\n\n return limiter\n })\n\n// ============================================\n// 4. Rate limit middleware\n// ============================================\n\nconst withRateLimit = (\n handler: Effect.Effect\n): Effect.Effect<\n A | HttpServerResponse.HttpServerResponse,\n E,\n R | RateLimiterService | HttpServerRequest.HttpServerRequest\n> =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const rateLimiter = yield* RateLimiterService\n\n // Use IP address as key (in production, might use user ID or API key)\n const clientKey = request.headers[\"x-forwarded-for\"] || \"unknown\"\n\n const result = yield* rateLimiter.check(clientKey).pipe(\n Effect.matchEffect({\n onFailure: (error) =>\n Effect.succeed(\n HttpServerResponse.json(\n {\n error: \"Rate limit exceeded\",\n retryAfter: error.retryAfter,\n },\n {\n status: 429,\n headers: {\n \"Retry-After\": String(error.retryAfter),\n \"X-RateLimit-Limit\": String(error.limit),\n \"X-RateLimit-Remaining\": \"0\",\n },\n }\n )\n ),\n onSuccess: () => handler,\n })\n )\n\n return result\n })\n\n// ============================================\n// 5. Usage example\n// ============================================\n\nconst RateLimiterLive = Layer.effect(\n RateLimiterService,\n makeRateLimiter({\n maxRequests: 100, // 100 requests\n windowMs: 60 * 1000, // per minute\n })\n)\n\nconst apiEndpoint = withRateLimit(\n Effect.gen(function* () {\n // Your actual handler logic\n return HttpServerResponse.json({ data: \"Success!\" })\n })\n)\n```", - "antiPattern": "", - "explanation": "Rate limiting protects your API:\n\n1. **Prevent abuse** - Stop malicious flooding\n2. **Fair usage** - Share resources among clients\n3. **Cost control** - Limit expensive operations\n4. **Stability** - Prevent cascading failures\n\n---", - "content": "## Guideline\n\nImplement rate limiting as a service that tracks request counts and enforces limits per client (IP, API key, or user).\n\n---\n\n## Rationale\n\nRate limiting protects your API:\n\n1. **Prevent abuse** - Stop malicious flooding\n2. **Fair usage** - Share resources among clients\n3. **Cost control** - Limit expensive operations\n4. **Stability** - Prevent cascading failures\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Context, Layer, Ref, HashMap, Data, Duration } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define rate limit types\n// ============================================\n\ninterface RateLimitConfig {\n readonly maxRequests: number\n readonly windowMs: number\n}\n\ninterface RateLimitState {\n readonly count: number\n readonly resetAt: number\n}\n\nclass RateLimitExceededError extends Data.TaggedError(\"RateLimitExceededError\")<{\n readonly retryAfter: number\n readonly limit: number\n}> {}\n\n// ============================================\n// 2. Rate limiter service\n// ============================================\n\ninterface RateLimiter {\n readonly check: (key: string) => Effect.Effect\n readonly getStatus: (key: string) => Effect.Effect<{\n remaining: number\n resetAt: number\n }>\n}\n\nclass RateLimiterService extends Context.Tag(\"RateLimiter\")<\n RateLimiterService,\n RateLimiter\n>() {}\n\n// ============================================\n// 3. In-memory rate limiter implementation\n// ============================================\n\nconst makeRateLimiter = (config: RateLimitConfig) =>\n Effect.gen(function* () {\n const state = yield* Ref.make(HashMap.empty())\n\n const getOrCreateState = (key: string, now: number) =>\n Ref.modify(state, (map) => {\n const existing = HashMap.get(map, key)\n\n if (existing._tag === \"Some\") {\n // Check if window expired\n if (now >= existing.value.resetAt) {\n // Start new window\n const newState: RateLimitState = {\n count: 0,\n resetAt: now + config.windowMs,\n }\n return [newState, HashMap.set(map, key, newState)]\n }\n return [existing.value, map]\n }\n\n // Create new entry\n const newState: RateLimitState = {\n count: 0,\n resetAt: now + config.windowMs,\n }\n return [newState, HashMap.set(map, key, newState)]\n })\n\n const incrementCount = (key: string) =>\n Ref.modify(state, (map) => {\n const existing = HashMap.get(map, key)\n if (existing._tag === \"Some\") {\n const updated = { ...existing.value, count: existing.value.count + 1 }\n return [updated.count, HashMap.set(map, key, updated)]\n }\n return [1, map]\n })\n\n const limiter: RateLimiter = {\n check: (key) =>\n Effect.gen(function* () {\n const now = Date.now()\n const currentState = yield* getOrCreateState(key, now)\n\n if (currentState.count >= config.maxRequests) {\n const retryAfter = Math.ceil((currentState.resetAt - now) / 1000)\n return yield* Effect.fail(\n new RateLimitExceededError({\n retryAfter,\n limit: config.maxRequests,\n })\n )\n }\n\n yield* incrementCount(key)\n }),\n\n getStatus: (key) =>\n Effect.gen(function* () {\n const now = Date.now()\n const currentState = yield* getOrCreateState(key, now)\n return {\n remaining: Math.max(0, config.maxRequests - currentState.count),\n resetAt: currentState.resetAt,\n }\n }),\n }\n\n return limiter\n })\n\n// ============================================\n// 4. Rate limit middleware\n// ============================================\n\nconst withRateLimit = (\n handler: Effect.Effect\n): Effect.Effect<\n A | HttpServerResponse.HttpServerResponse,\n E,\n R | RateLimiterService | HttpServerRequest.HttpServerRequest\n> =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const rateLimiter = yield* RateLimiterService\n\n // Use IP address as key (in production, might use user ID or API key)\n const clientKey = request.headers[\"x-forwarded-for\"] || \"unknown\"\n\n const result = yield* rateLimiter.check(clientKey).pipe(\n Effect.matchEffect({\n onFailure: (error) =>\n Effect.succeed(\n HttpServerResponse.json(\n {\n error: \"Rate limit exceeded\",\n retryAfter: error.retryAfter,\n },\n {\n status: 429,\n headers: {\n \"Retry-After\": String(error.retryAfter),\n \"X-RateLimit-Limit\": String(error.limit),\n \"X-RateLimit-Remaining\": \"0\",\n },\n }\n )\n ),\n onSuccess: () => handler,\n })\n )\n\n return result\n })\n\n// ============================================\n// 5. Usage example\n// ============================================\n\nconst RateLimiterLive = Layer.effect(\n RateLimiterService,\n makeRateLimiter({\n maxRequests: 100, // 100 requests\n windowMs: 60 * 1000, // per minute\n })\n)\n\nconst apiEndpoint = withRateLimit(\n Effect.gen(function* () {\n // Your actual handler logic\n return HttpServerResponse.json({ data: \"Success!\" })\n })\n)\n```\n\n## Rate Limit Strategies\n\n| Strategy | Key | Best For |\n|----------|-----|----------|\n| IP-based | Client IP | Public APIs |\n| User-based | User ID | Authenticated APIs |\n| API key | API key | B2B services |\n| Endpoint | IP + path | Per-endpoint limits |\n\n## Response Headers\n\n| Header | Purpose |\n|--------|---------|\n| `X-RateLimit-Limit` | Max requests allowed |\n| `X-RateLimit-Remaining` | Requests left |\n| `X-RateLimit-Reset` | When window resets |\n| `Retry-After` | Seconds until retry |\n\n## Best Practices\n\n1. **Return 429** - Standard \"Too Many Requests\" status\n2. **Include Retry-After** - Tell clients when to retry\n3. **Log violations** - Track abuse patterns\n4. **Tiered limits** - Different limits for different tiers\n5. **Graceful degradation** - Consider \"soft\" limits first" - }, - { - "id": "http-timeouts", - "title": "Add Timeouts to HTTP Requests", - "description": "Always set timeouts on HTTP requests to ensure your application doesn't hang.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Duration, Data } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Basic request timeout\n// ============================================\n\nconst fetchWithTimeout = (url: string, timeout: Duration.DurationInput) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.timeout(timeout)\n )\n // Returns Option - None if timed out\n })\n\n// ============================================\n// 2. Timeout with custom error\n// ============================================\n\nclass RequestTimeoutError extends Data.TaggedError(\"RequestTimeoutError\")<{\n readonly url: string\n readonly timeout: Duration.Duration\n}> {}\n\nconst fetchWithTimeoutError = (url: string, timeout: Duration.DurationInput) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.timeoutFail({\n duration: timeout,\n onTimeout: () => new RequestTimeoutError({\n url,\n timeout: Duration.decode(timeout),\n }),\n })\n )\n })\n\n// ============================================\n// 3. Different timeouts for different phases\n// ============================================\n\nconst fetchWithPhasedTimeouts = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n // Connection timeout (initial)\n const response = yield* client.get(url).pipe(\n Effect.timeout(\"5 seconds\"),\n Effect.flatten,\n Effect.mapError(() => new Error(\"Connection timeout\"))\n )\n\n // Read timeout (body)\n const body = yield* HttpClientResponse.text(response).pipe(\n Effect.timeout(\"30 seconds\"),\n Effect.flatten,\n Effect.mapError(() => new Error(\"Read timeout\"))\n )\n\n return body\n })\n\n// ============================================\n// 4. Timeout with fallback\n// ============================================\n\ninterface ApiResponse {\n data: unknown\n cached: boolean\n}\n\nconst fetchWithFallback = (url: string): Effect.Effect =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.map((data) => ({ data, cached: false })),\n Effect.timeout(\"5 seconds\"),\n Effect.flatMap((result) =>\n result._tag === \"Some\"\n ? Effect.succeed(result.value)\n : Effect.succeed({ data: null, cached: true }) // Fallback\n )\n )\n })\n\n// ============================================\n// 5. Timeout with interrupt\n// ============================================\n\nconst fetchWithInterrupt = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.interruptible,\n Effect.timeout(\"10 seconds\")\n )\n // Fiber is interrupted if timeout, freeing resources\n })\n\n// ============================================\n// 6. Configurable timeout wrapper\n// ============================================\n\ninterface TimeoutConfig {\n readonly connect: Duration.DurationInput\n readonly read: Duration.DurationInput\n readonly total: Duration.DurationInput\n}\n\nconst defaultTimeouts: TimeoutConfig = {\n connect: \"5 seconds\",\n read: \"30 seconds\",\n total: \"60 seconds\",\n}\n\nconst createHttpClient = (config: TimeoutConfig = defaultTimeouts) =>\n Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n baseClient.get(url).pipe(\n Effect.timeout(config.connect),\n Effect.flatten,\n Effect.flatMap((r) =>\n HttpClientResponse.json(r).pipe(\n Effect.timeout(config.read),\n Effect.flatten\n )\n ),\n Effect.timeout(config.total),\n Effect.flatten\n ),\n }\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Fetching with timeout...\")\n\n const result = yield* fetchWithTimeoutError(\n \"https://api.example.com/slow\",\n \"5 seconds\"\n ).pipe(\n Effect.catchTag(\"RequestTimeoutError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Request to ${error.url} timed out`)\n return { error: \"timeout\" }\n })\n )\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(result)}`)\n})\n```", - "antiPattern": "", - "explanation": "HTTP requests can hang indefinitely:\n\n1. **Server issues** - Unresponsive servers\n2. **Network problems** - Packets lost\n3. **Slow responses** - Large payloads\n4. **Resource leaks** - Connections never closed\n\nTimeouts prevent these from blocking your application.\n\n---", - "content": "## Guideline\n\nUse Effect's timeout functions to set limits on HTTP request duration, with appropriate fallback handling.\n\n---\n\n## Rationale\n\nHTTP requests can hang indefinitely:\n\n1. **Server issues** - Unresponsive servers\n2. **Network problems** - Packets lost\n3. **Slow responses** - Large payloads\n4. **Resource leaks** - Connections never closed\n\nTimeouts prevent these from blocking your application.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Duration, Data } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Basic request timeout\n// ============================================\n\nconst fetchWithTimeout = (url: string, timeout: Duration.DurationInput) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.timeout(timeout)\n )\n // Returns Option - None if timed out\n })\n\n// ============================================\n// 2. Timeout with custom error\n// ============================================\n\nclass RequestTimeoutError extends Data.TaggedError(\"RequestTimeoutError\")<{\n readonly url: string\n readonly timeout: Duration.Duration\n}> {}\n\nconst fetchWithTimeoutError = (url: string, timeout: Duration.DurationInput) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.timeoutFail({\n duration: timeout,\n onTimeout: () => new RequestTimeoutError({\n url,\n timeout: Duration.decode(timeout),\n }),\n })\n )\n })\n\n// ============================================\n// 3. Different timeouts for different phases\n// ============================================\n\nconst fetchWithPhasedTimeouts = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n // Connection timeout (initial)\n const response = yield* client.get(url).pipe(\n Effect.timeout(\"5 seconds\"),\n Effect.flatten,\n Effect.mapError(() => new Error(\"Connection timeout\"))\n )\n\n // Read timeout (body)\n const body = yield* HttpClientResponse.text(response).pipe(\n Effect.timeout(\"30 seconds\"),\n Effect.flatten,\n Effect.mapError(() => new Error(\"Read timeout\"))\n )\n\n return body\n })\n\n// ============================================\n// 4. Timeout with fallback\n// ============================================\n\ninterface ApiResponse {\n data: unknown\n cached: boolean\n}\n\nconst fetchWithFallback = (url: string): Effect.Effect =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.map((data) => ({ data, cached: false })),\n Effect.timeout(\"5 seconds\"),\n Effect.flatMap((result) =>\n result._tag === \"Some\"\n ? Effect.succeed(result.value)\n : Effect.succeed({ data: null, cached: true }) // Fallback\n )\n )\n })\n\n// ============================================\n// 5. Timeout with interrupt\n// ============================================\n\nconst fetchWithInterrupt = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.interruptible,\n Effect.timeout(\"10 seconds\")\n )\n // Fiber is interrupted if timeout, freeing resources\n })\n\n// ============================================\n// 6. Configurable timeout wrapper\n// ============================================\n\ninterface TimeoutConfig {\n readonly connect: Duration.DurationInput\n readonly read: Duration.DurationInput\n readonly total: Duration.DurationInput\n}\n\nconst defaultTimeouts: TimeoutConfig = {\n connect: \"5 seconds\",\n read: \"30 seconds\",\n total: \"60 seconds\",\n}\n\nconst createHttpClient = (config: TimeoutConfig = defaultTimeouts) =>\n Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n baseClient.get(url).pipe(\n Effect.timeout(config.connect),\n Effect.flatten,\n Effect.flatMap((r) =>\n HttpClientResponse.json(r).pipe(\n Effect.timeout(config.read),\n Effect.flatten\n )\n ),\n Effect.timeout(config.total),\n Effect.flatten\n ),\n }\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Fetching with timeout...\")\n\n const result = yield* fetchWithTimeoutError(\n \"https://api.example.com/slow\",\n \"5 seconds\"\n ).pipe(\n Effect.catchTag(\"RequestTimeoutError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Request to ${error.url} timed out`)\n return { error: \"timeout\" }\n })\n )\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(result)}`)\n})\n```\n\n## Timeout Types\n\n| Timeout | What It Limits |\n|---------|----------------|\n| **Connect** | Time to establish connection |\n| **Read** | Time to read response body |\n| **Total** | Entire request duration |\n| **Idle** | Time between data packets |\n\n## Timeout Functions\n\n| Function | Behavior |\n|----------|----------|\n| `Effect.timeout` | Returns Option |\n| `Effect.timeoutFail` | Fails with custom error |\n| `Effect.timeoutTo` | Returns fallback value |\n| `Effect.disconnect` | Interrupt and return |\n\n## Best Practices\n\n1. **Always set timeouts** - Never wait forever\n2. **Use appropriate values** - Too short = false failures\n3. **Layer timeouts** - Connect, read, total\n4. **Handle gracefully** - Fallbacks or clear errors\n5. **Log timeouts** - Track slow endpoints" - }, - { - "id": "stream-retry-on-failure", - "title": "Automatically Retry Failed Operations", - "description": "Compose a Stream with the .retry(Schedule) operator to automatically recover from transient failures.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { Effect, Stream, Schedule } from \"effect\";\n\n// A mock function that simulates a flaky API call\nconst processItem = (id: number): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`Attempting to process item ${id}...`);\n\n // Item 2 fails on first attempt but succeeds on retry\n if (id === 2) {\n const random = Math.random();\n if (random < 0.5) {\n // 50% chance of failure for demonstration\n yield* Effect.log(`Item ${id} failed, will retry...`);\n return yield* Effect.fail(new Error(\"API is temporarily down\"));\n }\n }\n\n yield* Effect.log(`✅ Successfully processed item ${id}`);\n return `Processed item ${id}`;\n });\n\nconst ids = [1, 2, 3];\n\n// Define a retry policy: 3 attempts with a fixed 100ms delay\nconst retryPolicy = Schedule.recurs(3).pipe(\n Schedule.addDelay(() => \"100 millis\")\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Stream Retry on Failure Demo ===\");\n yield* Effect.log(\n \"Processing items with retry policy (3 attempts, 100ms delay)\"\n );\n\n // Process each item individually with retry\n const results = yield* Effect.forEach(\n ids,\n (id) =>\n processItem(id).pipe(\n Effect.retry(retryPolicy),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `❌ Item ${id} failed after all retries: ${error.message}`\n );\n return `Failed: item ${id}`;\n })\n )\n ),\n { concurrency: 1 }\n );\n\n yield* Effect.log(\"=== Results ===\");\n for (let index = 0; index < results.length; index++) {\n yield* Effect.log(`Item ${ids[index]}: ${results[index]}`);\n }\n\n yield* Effect.log(\"✅ Stream processing completed\");\n});\n\nEffect.runPromise(program).catch((error) => {\n Effect.runSync(Effect.logError(\"Unexpected error: \" + error));\n});\n/*\nOutput:\n... level=INFO msg=\"Attempting to process item 1...\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 1.\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 2.\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Attempting to process item 3...\"\n*/\n```", - "antiPattern": "The anti-pattern is to either have no retry logic at all, or to write manual, imperative retry loops inside your processing function.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same mock processItem function ...\n\nconst ids = [1, 2, 3];\n\nconst program = Stream.fromIterable(ids).pipe(\n // No retry logic. The entire stream will fail when item 2 fails.\n Stream.mapEffect(processItem, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program).catch((error) => {\n console.error(\"Pipeline failed:\", error);\n});\n/*\nOutput:\n... level=INFO msg=\"Attempting to process item 1...\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 1.\"\nPipeline failed: Error: API is temporarily down\n*/\n```\n\nThis \"fail-fast\" approach is brittle. A single, temporary network blip would cause the entire pipeline to terminate, even if subsequent items could have been processed successfully. While manual retry logic inside `processItem` is possible, it pollutes the core logic with concerns about timing and attempt counting, and is far less composable and reusable than a `Schedule`.", - "explanation": "Real-world systems are unreliable. Network connections drop, APIs return temporary `503` errors, and databases can experience deadlocks. A naive pipeline will fail completely on the first sign of trouble. A resilient pipeline, however, can absorb these transient errors and heal itself.\n\nThe `retry` operator, combined with the `Schedule` module, provides a powerful and declarative way to build this resilience:\n\n1. **Declarative Resilience**: Instead of writing complex `try/catch` loops with manual delay logic, you declaratively state _how_ the pipeline should retry. For example, \"retry 3 times, with an exponential backoff starting at 100ms.\"\n2. **Separation of Concerns**: Your core pipeline logic remains focused on the \"happy path.\" The retry strategy is a separate, composable concern that you apply to the entire stream.\n3. **Rich Scheduling Policies**: `Schedule` is incredibly powerful. You can create schedules based on a fixed number of retries, exponential backoff, jitter (to avoid thundering herd problems), or even combinations of these.\n4. **Prevents Cascading Failures**: By handling temporary issues at the source, you prevent a small, transient glitch from causing a complete failure of your entire application.\n\n---", - "content": "## Guideline\n\nTo make a data pipeline resilient to transient failures, apply the `.retry(Schedule)` operator to the `Stream`.\n\n---\n\n## Rationale\n\nReal-world systems are unreliable. Network connections drop, APIs return temporary `503` errors, and databases can experience deadlocks. A naive pipeline will fail completely on the first sign of trouble. A resilient pipeline, however, can absorb these transient errors and heal itself.\n\nThe `retry` operator, combined with the `Schedule` module, provides a powerful and declarative way to build this resilience:\n\n1. **Declarative Resilience**: Instead of writing complex `try/catch` loops with manual delay logic, you declaratively state _how_ the pipeline should retry. For example, \"retry 3 times, with an exponential backoff starting at 100ms.\"\n2. **Separation of Concerns**: Your core pipeline logic remains focused on the \"happy path.\" The retry strategy is a separate, composable concern that you apply to the entire stream.\n3. **Rich Scheduling Policies**: `Schedule` is incredibly powerful. You can create schedules based on a fixed number of retries, exponential backoff, jitter (to avoid thundering herd problems), or even combinations of these.\n4. **Prevents Cascading Failures**: By handling temporary issues at the source, you prevent a small, transient glitch from causing a complete failure of your entire application.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream, Schedule } from \"effect\";\n\n// A mock function that simulates a flaky API call\nconst processItem = (id: number): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`Attempting to process item ${id}...`);\n\n // Item 2 fails on first attempt but succeeds on retry\n if (id === 2) {\n const random = Math.random();\n if (random < 0.5) {\n // 50% chance of failure for demonstration\n yield* Effect.log(`Item ${id} failed, will retry...`);\n return yield* Effect.fail(new Error(\"API is temporarily down\"));\n }\n }\n\n yield* Effect.log(`✅ Successfully processed item ${id}`);\n return `Processed item ${id}`;\n });\n\nconst ids = [1, 2, 3];\n\n// Define a retry policy: 3 attempts with a fixed 100ms delay\nconst retryPolicy = Schedule.recurs(3).pipe(\n Schedule.addDelay(() => \"100 millis\")\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Stream Retry on Failure Demo ===\");\n yield* Effect.log(\n \"Processing items with retry policy (3 attempts, 100ms delay)\"\n );\n\n // Process each item individually with retry\n const results = yield* Effect.forEach(\n ids,\n (id) =>\n processItem(id).pipe(\n Effect.retry(retryPolicy),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `❌ Item ${id} failed after all retries: ${error.message}`\n );\n return `Failed: item ${id}`;\n })\n )\n ),\n { concurrency: 1 }\n );\n\n yield* Effect.log(\"=== Results ===\");\n for (let index = 0; index < results.length; index++) {\n yield* Effect.log(`Item ${ids[index]}: ${results[index]}`);\n }\n\n yield* Effect.log(\"✅ Stream processing completed\");\n});\n\nEffect.runPromise(program).catch((error) => {\n Effect.runSync(Effect.logError(\"Unexpected error: \" + error));\n});\n/*\nOutput:\n... level=INFO msg=\"Attempting to process item 1...\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 1.\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 2.\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Attempting to process item 3...\"\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to either have no retry logic at all, or to write manual, imperative retry loops inside your processing function.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same mock processItem function ...\n\nconst ids = [1, 2, 3];\n\nconst program = Stream.fromIterable(ids).pipe(\n // No retry logic. The entire stream will fail when item 2 fails.\n Stream.mapEffect(processItem, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program).catch((error) => {\n console.error(\"Pipeline failed:\", error);\n});\n/*\nOutput:\n... level=INFO msg=\"Attempting to process item 1...\"\n... level=INFO msg=\"Attempting to process item 2...\"\n... level=INFO msg=\"Item 2 failed, attempt 1.\"\nPipeline failed: Error: API is temporarily down\n*/\n```\n\nThis \"fail-fast\" approach is brittle. A single, temporary network blip would cause the entire pipeline to terminate, even if subsequent items could have been processed successfully. While manual retry logic inside `processItem` is possible, it pollutes the core logic with concerns about timing and attempt counting, and is far less composable and reusable than a `Schedule`." - }, - { - "id": "avoid-long-andthen-chains", - "title": "Avoid Long Chains of .andThen; Use Generators Instead", - "description": "Prefer generators over long chains of .andThen.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Define our steps with logging\nconst step1 = (): Effect.Effect =>\n Effect.succeed(42).pipe(Effect.tap((n) => Effect.log(`Step 1: ${n}`)));\n\nconst step2 = (a: number): Effect.Effect =>\n Effect.succeed(`Result: ${a * 2}`).pipe(\n Effect.tap((s) => Effect.log(`Step 2: ${s}`))\n );\n\n// Using Effect.gen for better readability\nconst program = Effect.gen(function* () {\n const a = yield* step1();\n const b = yield* step2(a);\n return b;\n});\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Final result: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \nGenerators keep sequential logic readable and easy to maintain.", - "antiPattern": "```typescript\nimport { Effect } from \"effect\";\ndeclare const step1: () => Effect.Effect;\ndeclare const step2: (a: any) => Effect.Effect;\n\nstep1().pipe(Effect.flatMap((a) => step2(a))); // Or .andThen\n```\n\nChaining many `.flatMap` or `.andThen` calls leads to deeply nested,\nhard-to-read code.", - "explanation": "`Effect.gen` provides a flat, linear code structure that is easier to read and\ndebug than deeply nested functional chains.", - "content": "# Avoid Long Chains of .andThen; Use Generators Instead\n\n## Guideline\n\nFor sequential logic involving more than two steps, prefer `Effect.gen` over\nchaining multiple `.andThen` or `.flatMap` calls.\n\n## Rationale\n\n`Effect.gen` provides a flat, linear code structure that is easier to read and\ndebug than deeply nested functional chains.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define our steps with logging\nconst step1 = (): Effect.Effect =>\n Effect.succeed(42).pipe(Effect.tap((n) => Effect.log(`Step 1: ${n}`)));\n\nconst step2 = (a: number): Effect.Effect =>\n Effect.succeed(`Result: ${a * 2}`).pipe(\n Effect.tap((s) => Effect.log(`Step 2: ${s}`))\n );\n\n// Using Effect.gen for better readability\nconst program = Effect.gen(function* () {\n const a = yield* step1();\n const b = yield* step2(a);\n return b;\n});\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Final result: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \nGenerators keep sequential logic readable and easy to maintain.\n\n## Anti-Pattern\n\n```typescript\nimport { Effect } from \"effect\";\ndeclare const step1: () => Effect.Effect;\ndeclare const step2: (a: any) => Effect.Effect;\n\nstep1().pipe(Effect.flatMap((a) => step2(a))); // Or .andThen\n```\n\nChaining many `.flatMap` or `.andThen` calls leads to deeply nested,\nhard-to-read code." - }, - { - "id": "beyond-the-date-type", - "title": "Beyond the Date Type - Real World Dates, Times, and Timezones", - "description": "Use the Clock service for testable time-based logic and immutable primitives for timestamps.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "This example shows a function that creates a timestamped event. It depends on the `Clock` service, making it fully testable.\n\n```typescript\nimport { Effect, Clock } from \"effect\";\nimport type * as Types from \"effect/Clock\";\n\ninterface Event {\n readonly message: string;\n readonly timestamp: number; // Store as a primitive number (UTC millis)\n}\n\n// This function is pure and testable because it depends on Clock\nconst createEvent = (\n message: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const timestamp = yield* Clock.currentTimeMillis;\n return { message, timestamp };\n });\n\n// Create and log some events\nconst program = Effect.gen(function* () {\n const loginEvent = yield* createEvent(\"User logged in\");\n yield* Effect.log(\"Login event:\", loginEvent);\n\n const logoutEvent = yield* createEvent(\"User logged out\");\n yield* Effect.log(\"Logout event:\", logoutEvent);\n});\n\n// Run the program\nconst programWithErrorHandling = program.pipe(\n Effect.provideService(Clock.Clock, Clock.make()),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n---", - "antiPattern": "Directly using `Date.now()` or `new Date()` inside your effects. This introduces impurity and makes your logic dependent on the actual system clock, rendering it non-deterministic and difficult to test.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This function is impure and not reliably testable.\nconst createEventUnsafely = (message: string): Effect.Effect =>\n Effect.sync(() => ({\n message,\n timestamp: Date.now(), // Direct call to a system API\n }));\n\n// How would you test that this function assigns the correct timestamp\n// without manipulating the system clock or using complex mocks?\n```", - "explanation": "JavaScript's native `Date` object is a common source of bugs. It is mutable, its behavior can be inconsistent across different JavaScript environments (especially with timezones), and its reliance on the system clock makes time-dependent logic difficult to test.\n\nEffect's approach solves these problems:\n\n- The **`Clock` service** abstracts away the concept of \"now.\" In production, the `Live` clock uses the system time. In tests, you can provide a `TestClock` that gives you complete, deterministic control over the passage of time.\n- Using **primitive `number` or `string`** for timestamps ensures immutability and makes your data easy to serialize, store, and transfer.\n\nThis makes your time-based logic pure, predictable, and easy to test.\n\n---", - "content": "## Guideline\n\nTo handle specific points in time robustly in Effect, follow these principles:\n\n1. **Access \"now\" via the `Clock` service** (`Clock.currentTimeMillis`) instead of `Date.now()`.\n2. **Store and pass timestamps** as immutable primitives: `number` for UTC milliseconds or `string` for ISO 8601 format.\n3. **Perform calculations locally:** When you need to perform date-specific calculations (e.g., \"get the day of the week\"), create a `new Date(timestamp)` instance inside a pure computation, use it, and then discard it. Never hold onto mutable `Date` objects in your application state.\n\n---\n\n## Rationale\n\nJavaScript's native `Date` object is a common source of bugs. It is mutable, its behavior can be inconsistent across different JavaScript environments (especially with timezones), and its reliance on the system clock makes time-dependent logic difficult to test.\n\nEffect's approach solves these problems:\n\n- The **`Clock` service** abstracts away the concept of \"now.\" In production, the `Live` clock uses the system time. In tests, you can provide a `TestClock` that gives you complete, deterministic control over the passage of time.\n- Using **primitive `number` or `string`** for timestamps ensures immutability and makes your data easy to serialize, store, and transfer.\n\nThis makes your time-based logic pure, predictable, and easy to test.\n\n---\n\n## Good Example\n\nThis example shows a function that creates a timestamped event. It depends on the `Clock` service, making it fully testable.\n\n```typescript\nimport { Effect, Clock } from \"effect\";\nimport type * as Types from \"effect/Clock\";\n\ninterface Event {\n readonly message: string;\n readonly timestamp: number; // Store as a primitive number (UTC millis)\n}\n\n// This function is pure and testable because it depends on Clock\nconst createEvent = (\n message: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const timestamp = yield* Clock.currentTimeMillis;\n return { message, timestamp };\n });\n\n// Create and log some events\nconst program = Effect.gen(function* () {\n const loginEvent = yield* createEvent(\"User logged in\");\n yield* Effect.log(\"Login event:\", loginEvent);\n\n const logoutEvent = yield* createEvent(\"User logged out\");\n yield* Effect.log(\"Logout event:\", logoutEvent);\n});\n\n// Run the program\nconst programWithErrorHandling = program.pipe(\n Effect.provideService(Clock.Clock, Clock.make()),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n---\n\n## Anti-Pattern\n\nDirectly using `Date.now()` or `new Date()` inside your effects. This introduces impurity and makes your logic dependent on the actual system clock, rendering it non-deterministic and difficult to test.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This function is impure and not reliably testable.\nconst createEventUnsafely = (message: string): Effect.Effect =>\n Effect.sync(() => ({\n message,\n timestamp: Date.now(), // Direct call to a system API\n }));\n\n// How would you test that this function assigns the correct timestamp\n// without manipulating the system clock or using complex mocks?\n```" - }, - { - "id": "build-a-basic-http-server", - "title": "Build a Basic HTTP Server", - "description": "Use a managed Runtime created from a Layer to handle requests in a Node.js HTTP server.", - "skillLevel": "advanced", - "useCases": [ - "making-http-requests" - ], - "example": "This example creates a simple server with a `Greeter` service. The server starts, creates a runtime containing the `Greeter`, and then uses that runtime to handle requests.\n\n```typescript\nimport { HttpServer, HttpServerResponse } from \"@effect/platform\";\nimport { NodeHttpServer } from \"@effect/platform-node\";\nimport { Duration, Effect, Fiber, Layer } from \"effect\";\nimport { createServer } from \"node:http\";\n\n// Create a server layer using Node's built-in HTTP server\nconst ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3001 });\n\n// Define your HTTP app (here responding \"Hello World\" to every request)\nconst app = Effect.gen(function* () {\n yield* Effect.logInfo(\"Received HTTP request\");\n return yield* HttpServerResponse.text(\"Hello World\");\n});\n\nconst serverLayer = HttpServer.serve(app).pipe(Layer.provide(ServerLive));\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Server starting on http://localhost:3001\");\n const fiber = yield* Layer.launch(serverLayer).pipe(Effect.fork);\n yield* Effect.sleep(Duration.seconds(2));\n yield* Fiber.interrupt(fiber);\n yield* Effect.logInfo(\"Server shutdown complete\");\n});\n\nEffect.runPromise(program as unknown as Effect.Effect);\n```\n\n---", - "antiPattern": "Creating a new runtime or rebuilding layers for every single incoming request. This is extremely inefficient and defeats the purpose of Effect's `Layer` system.\n\n```typescript\nimport * as http from \"http\";\nimport { Effect, Layer } from \"effect\";\nimport { GreeterLive } from \"./somewhere\";\n\n// ❌ WRONG: This rebuilds the GreeterLive layer on every request.\nconst server = http.createServer((_req, res) => {\n const requestEffect = Effect.succeed(\"Hello!\").pipe(\n Effect.provide(GreeterLive) // Providing the layer here is inefficient\n );\n Effect.runPromise(requestEffect).then((msg) => res.end(msg));\n});\n```", - "explanation": "This pattern demonstrates the complete lifecycle of a long-running Effect application.\n\n1. **Setup Phase:** You define all your application's dependencies (database connections, clients, config) in `Layer`s and compose them into a single `AppLayer`.\n2. **Runtime Creation:** You use `Layer.toRuntime(AppLayer)` to create a highly-optimized `Runtime` object. This is done _once_ when the server starts.\n3. **Request Handling:** For each incoming request, you create an `Effect` that describes the work to be done (e.g., parse request, call services, create response).\n4. **Execution:** You use the `Runtime` you created in the setup phase to execute the request-handling `Effect` using `Runtime.runPromise`.\n\nThis architecture ensures that your request handling logic is fully testable, benefits from structured concurrency, and is completely decoupled from the server's setup and infrastructure.\n\n---", - "content": "## Guideline\n\nTo build an HTTP server, create a main `AppLayer` that provides all your application's services. Compile this layer into a managed `Runtime` at startup. Use this runtime to execute an `Effect` for each incoming HTTP request, ensuring all logic is handled within the Effect system.\n\n---\n\n## Rationale\n\nThis pattern demonstrates the complete lifecycle of a long-running Effect application.\n\n1. **Setup Phase:** You define all your application's dependencies (database connections, clients, config) in `Layer`s and compose them into a single `AppLayer`.\n2. **Runtime Creation:** You use `Layer.toRuntime(AppLayer)` to create a highly-optimized `Runtime` object. This is done _once_ when the server starts.\n3. **Request Handling:** For each incoming request, you create an `Effect` that describes the work to be done (e.g., parse request, call services, create response).\n4. **Execution:** You use the `Runtime` you created in the setup phase to execute the request-handling `Effect` using `Runtime.runPromise`.\n\nThis architecture ensures that your request handling logic is fully testable, benefits from structured concurrency, and is completely decoupled from the server's setup and infrastructure.\n\n---\n\n## Good Example\n\nThis example creates a simple server with a `Greeter` service. The server starts, creates a runtime containing the `Greeter`, and then uses that runtime to handle requests.\n\n```typescript\nimport { HttpServer, HttpServerResponse } from \"@effect/platform\";\nimport { NodeHttpServer } from \"@effect/platform-node\";\nimport { Duration, Effect, Fiber, Layer } from \"effect\";\nimport { createServer } from \"node:http\";\n\n// Create a server layer using Node's built-in HTTP server\nconst ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3001 });\n\n// Define your HTTP app (here responding \"Hello World\" to every request)\nconst app = Effect.gen(function* () {\n yield* Effect.logInfo(\"Received HTTP request\");\n return yield* HttpServerResponse.text(\"Hello World\");\n});\n\nconst serverLayer = HttpServer.serve(app).pipe(Layer.provide(ServerLive));\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Server starting on http://localhost:3001\");\n const fiber = yield* Layer.launch(serverLayer).pipe(Effect.fork);\n yield* Effect.sleep(Duration.seconds(2));\n yield* Fiber.interrupt(fiber);\n yield* Effect.logInfo(\"Server shutdown complete\");\n});\n\nEffect.runPromise(program as unknown as Effect.Effect);\n```\n\n---\n\n## Anti-Pattern\n\nCreating a new runtime or rebuilding layers for every single incoming request. This is extremely inefficient and defeats the purpose of Effect's `Layer` system.\n\n```typescript\nimport * as http from \"http\";\nimport { Effect, Layer } from \"effect\";\nimport { GreeterLive } from \"./somewhere\";\n\n// ❌ WRONG: This rebuilds the GreeterLive layer on every request.\nconst server = http.createServer((_req, res) => {\n const requestEffect = Effect.succeed(\"Hello!\").pipe(\n Effect.provide(GreeterLive) // Providing the layer here is inefficient\n );\n Effect.runPromise(requestEffect).then((msg) => res.end(msg));\n});\n```" - }, - { - "id": "http-caching", - "title": "Cache HTTP Responses", - "description": "Use an in-memory or persistent cache to store HTTP responses.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Ref, HashMap, Option, Duration } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Simple in-memory cache\n// ============================================\n\ninterface CacheEntry {\n readonly data: T\n readonly timestamp: number\n readonly ttl: number\n}\n\nconst makeCache = () =>\n Effect.gen(function* () {\n const store = yield* Ref.make(HashMap.empty>())\n\n const get = (key: string): Effect.Effect> =>\n Ref.get(store).pipe(\n Effect.map((map) => {\n const entry = HashMap.get(map, key)\n if (entry._tag === \"None\") return Option.none()\n\n const now = Date.now()\n if (now > entry.value.timestamp + entry.value.ttl) {\n return Option.none() // Expired\n }\n return Option.some(entry.value.data)\n })\n )\n\n const set = (key: string, data: T, ttl: number): Effect.Effect =>\n Ref.update(store, (map) =>\n HashMap.set(map, key, {\n data,\n timestamp: Date.now(),\n ttl,\n })\n )\n\n const invalidate = (key: string): Effect.Effect =>\n Ref.update(store, (map) => HashMap.remove(map, key))\n\n const clear = (): Effect.Effect =>\n Ref.set(store, HashMap.empty())\n\n return { get, set, invalidate, clear }\n })\n\n// ============================================\n// 2. Cached HTTP client\n// ============================================\n\ninterface CachedHttpClient {\n readonly get: (\n url: string,\n options?: { ttl?: Duration.DurationInput }\n ) => Effect.Effect\n readonly invalidate: (url: string) => Effect.Effect\n}\n\nconst makeCachedHttpClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const cache = yield* makeCache()\n\n const client: CachedHttpClient = {\n get: (url: string, options?: { ttl?: Duration.DurationInput }) => {\n const ttl = options?.ttl ? Duration.toMillis(Duration.decode(options.ttl)) : 60000\n\n return Effect.gen(function* () {\n // Check cache first\n const cached = yield* cache.get(url)\n if (Option.isSome(cached)) {\n yield* Effect.log(`Cache hit: ${url}`)\n return cached.value as T\n }\n\n yield* Effect.log(`Cache miss: ${url}`)\n\n // Fetch from network\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n\n // Store in cache\n yield* cache.set(url, data, ttl)\n\n return data\n })\n },\n\n invalidate: (url) => cache.invalidate(url),\n }\n\n return client\n})\n\n// ============================================\n// 3. Stale-while-revalidate pattern\n// ============================================\n\ninterface SWRCache {\n readonly data: T\n readonly timestamp: number\n readonly staleAfter: number\n readonly expireAfter: number\n}\n\nconst makeSWRClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const cache = yield* Ref.make(HashMap.empty>())\n\n return {\n get: (\n url: string,\n options: {\n staleAfter: Duration.DurationInput\n expireAfter: Duration.DurationInput\n }\n ) =>\n Effect.gen(function* () {\n const now = Date.now()\n const staleMs = Duration.toMillis(Duration.decode(options.staleAfter))\n const expireMs = Duration.toMillis(Duration.decode(options.expireAfter))\n\n const cached = yield* Ref.get(cache).pipe(\n Effect.map((map) => HashMap.get(map, url))\n )\n\n if (cached._tag === \"Some\") {\n const entry = cached.value\n const age = now - entry.timestamp\n\n if (age < staleMs) {\n // Fresh - return immediately\n return entry.data as T\n }\n\n if (age < expireMs) {\n // Stale - return cached, revalidate in background\n yield* Effect.fork(\n httpClient.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.flatMap((data) =>\n Ref.update(cache, (map) =>\n HashMap.set(map, url, {\n data,\n timestamp: Date.now(),\n staleAfter: staleMs,\n expireAfter: expireMs,\n })\n )\n ),\n Effect.catchAll(() => Effect.void) // Ignore errors\n )\n )\n return entry.data as T\n }\n }\n\n // Expired or missing - fetch fresh\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n\n yield* Ref.update(cache, (map) =>\n HashMap.set(map, url, {\n data,\n timestamp: now,\n staleAfter: staleMs,\n expireAfter: expireMs,\n })\n )\n\n return data\n }),\n }\n})\n\n// ============================================\n// 4. Cache with request deduplication\n// ============================================\n\nconst makeDeduplicatedClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const inFlight = yield* Ref.make(HashMap.empty>())\n const cache = yield* makeCache()\n\n return {\n get: (url: string, ttl: number = 60000) =>\n Effect.gen(function* () {\n // Check cache\n const cached = yield* cache.get(url)\n if (Option.isSome(cached)) {\n return cached.value as T\n }\n\n // Check if request already in flight\n const pending = yield* Ref.get(inFlight).pipe(\n Effect.map((map) => HashMap.get(map, url))\n )\n\n if (pending._tag === \"Some\") {\n yield* Effect.log(`Deduplicating request: ${url}`)\n return (yield* pending.value) as T\n }\n\n // Make the request\n const request = httpClient.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.tap((data) => cache.set(url, data, ttl)),\n Effect.ensuring(\n Ref.update(inFlight, (map) => HashMap.remove(map, url))\n )\n )\n\n // Store in-flight request\n yield* Ref.update(inFlight, (map) => HashMap.set(map, url, request))\n\n return (yield* request) as T\n }),\n }\n})\n\n// ============================================\n// 5. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeCachedHttpClient\n\n // First call - cache miss\n yield* client.get(\"https://api.example.com/users/1\", { ttl: \"5 minutes\" })\n\n // Second call - cache hit\n yield* client.get(\"https://api.example.com/users/1\")\n\n // Invalidate when data changes\n yield* client.invalidate(\"https://api.example.com/users/1\")\n})\n```", - "antiPattern": "", - "explanation": "Caching provides:\n\n1. **Performance** - Avoid redundant network calls\n2. **Cost reduction** - Fewer API calls\n3. **Resilience** - Serve stale data when API is down\n4. **Rate limit safety** - Stay under quotas\n\n---", - "content": "## Guideline\n\nCache HTTP responses to reduce network calls, improve latency, and handle offline scenarios.\n\n---\n\n## Rationale\n\nCaching provides:\n\n1. **Performance** - Avoid redundant network calls\n2. **Cost reduction** - Fewer API calls\n3. **Resilience** - Serve stale data when API is down\n4. **Rate limit safety** - Stay under quotas\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Ref, HashMap, Option, Duration } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Simple in-memory cache\n// ============================================\n\ninterface CacheEntry {\n readonly data: T\n readonly timestamp: number\n readonly ttl: number\n}\n\nconst makeCache = () =>\n Effect.gen(function* () {\n const store = yield* Ref.make(HashMap.empty>())\n\n const get = (key: string): Effect.Effect> =>\n Ref.get(store).pipe(\n Effect.map((map) => {\n const entry = HashMap.get(map, key)\n if (entry._tag === \"None\") return Option.none()\n\n const now = Date.now()\n if (now > entry.value.timestamp + entry.value.ttl) {\n return Option.none() // Expired\n }\n return Option.some(entry.value.data)\n })\n )\n\n const set = (key: string, data: T, ttl: number): Effect.Effect =>\n Ref.update(store, (map) =>\n HashMap.set(map, key, {\n data,\n timestamp: Date.now(),\n ttl,\n })\n )\n\n const invalidate = (key: string): Effect.Effect =>\n Ref.update(store, (map) => HashMap.remove(map, key))\n\n const clear = (): Effect.Effect =>\n Ref.set(store, HashMap.empty())\n\n return { get, set, invalidate, clear }\n })\n\n// ============================================\n// 2. Cached HTTP client\n// ============================================\n\ninterface CachedHttpClient {\n readonly get: (\n url: string,\n options?: { ttl?: Duration.DurationInput }\n ) => Effect.Effect\n readonly invalidate: (url: string) => Effect.Effect\n}\n\nconst makeCachedHttpClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const cache = yield* makeCache()\n\n const client: CachedHttpClient = {\n get: (url: string, options?: { ttl?: Duration.DurationInput }) => {\n const ttl = options?.ttl ? Duration.toMillis(Duration.decode(options.ttl)) : 60000\n\n return Effect.gen(function* () {\n // Check cache first\n const cached = yield* cache.get(url)\n if (Option.isSome(cached)) {\n yield* Effect.log(`Cache hit: ${url}`)\n return cached.value as T\n }\n\n yield* Effect.log(`Cache miss: ${url}`)\n\n // Fetch from network\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n\n // Store in cache\n yield* cache.set(url, data, ttl)\n\n return data\n })\n },\n\n invalidate: (url) => cache.invalidate(url),\n }\n\n return client\n})\n\n// ============================================\n// 3. Stale-while-revalidate pattern\n// ============================================\n\ninterface SWRCache {\n readonly data: T\n readonly timestamp: number\n readonly staleAfter: number\n readonly expireAfter: number\n}\n\nconst makeSWRClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const cache = yield* Ref.make(HashMap.empty>())\n\n return {\n get: (\n url: string,\n options: {\n staleAfter: Duration.DurationInput\n expireAfter: Duration.DurationInput\n }\n ) =>\n Effect.gen(function* () {\n const now = Date.now()\n const staleMs = Duration.toMillis(Duration.decode(options.staleAfter))\n const expireMs = Duration.toMillis(Duration.decode(options.expireAfter))\n\n const cached = yield* Ref.get(cache).pipe(\n Effect.map((map) => HashMap.get(map, url))\n )\n\n if (cached._tag === \"Some\") {\n const entry = cached.value\n const age = now - entry.timestamp\n\n if (age < staleMs) {\n // Fresh - return immediately\n return entry.data as T\n }\n\n if (age < expireMs) {\n // Stale - return cached, revalidate in background\n yield* Effect.fork(\n httpClient.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.flatMap((data) =>\n Ref.update(cache, (map) =>\n HashMap.set(map, url, {\n data,\n timestamp: Date.now(),\n staleAfter: staleMs,\n expireAfter: expireMs,\n })\n )\n ),\n Effect.catchAll(() => Effect.void) // Ignore errors\n )\n )\n return entry.data as T\n }\n }\n\n // Expired or missing - fetch fresh\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n\n yield* Ref.update(cache, (map) =>\n HashMap.set(map, url, {\n data,\n timestamp: now,\n staleAfter: staleMs,\n expireAfter: expireMs,\n })\n )\n\n return data\n }),\n }\n})\n\n// ============================================\n// 4. Cache with request deduplication\n// ============================================\n\nconst makeDeduplicatedClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const inFlight = yield* Ref.make(HashMap.empty>())\n const cache = yield* makeCache()\n\n return {\n get: (url: string, ttl: number = 60000) =>\n Effect.gen(function* () {\n // Check cache\n const cached = yield* cache.get(url)\n if (Option.isSome(cached)) {\n return cached.value as T\n }\n\n // Check if request already in flight\n const pending = yield* Ref.get(inFlight).pipe(\n Effect.map((map) => HashMap.get(map, url))\n )\n\n if (pending._tag === \"Some\") {\n yield* Effect.log(`Deduplicating request: ${url}`)\n return (yield* pending.value) as T\n }\n\n // Make the request\n const request = httpClient.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.tap((data) => cache.set(url, data, ttl)),\n Effect.ensuring(\n Ref.update(inFlight, (map) => HashMap.remove(map, url))\n )\n )\n\n // Store in-flight request\n yield* Ref.update(inFlight, (map) => HashMap.set(map, url, request))\n\n return (yield* request) as T\n }),\n }\n})\n\n// ============================================\n// 5. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeCachedHttpClient\n\n // First call - cache miss\n yield* client.get(\"https://api.example.com/users/1\", { ttl: \"5 minutes\" })\n\n // Second call - cache hit\n yield* client.get(\"https://api.example.com/users/1\")\n\n // Invalidate when data changes\n yield* client.invalidate(\"https://api.example.com/users/1\")\n})\n```\n\n## Caching Strategies\n\n| Strategy | Behavior | Use When |\n|----------|----------|----------|\n| **TTL** | Expire after time | Most cases |\n| **Stale-while-revalidate** | Return stale, refresh async | UX priority |\n| **Cache-first** | Always cache, refresh manually | Offline support |\n| **Network-first** | Try network, fallback to cache | Fresh data priority |\n\n## Best Practices\n\n1. **Set appropriate TTL** - Balance freshness and performance\n2. **Deduplicate requests** - Don't fetch same URL multiple times\n3. **Handle cache misses** - Network can fail\n4. **Invalidate on mutations** - POST/PUT/DELETE should clear cache\n5. **Monitor hit rates** - Know if caching helps" - }, - { - "id": "combinator-flatmap", - "title": "Chaining Computations with flatMap", - "description": "Use flatMap to sequence computations, flattening nested structures and preserving error and context handling.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Chain two effectful computations\nconst effect = Effect.succeed(2).pipe(\n Effect.flatMap((n) => Effect.succeed(n * 10))\n); // Effect\n\n// Option: Chain two optional computations\nconst option = Option.some(2).pipe(Option.flatMap((n) => Option.some(n * 10))); // Option\n\n// Either: Chain two computations that may fail\nconst either = Either.right(2).pipe(\n Either.flatMap((n) => Either.right(n * 10))\n); // Either\n\n// Stream: Chain streams (flattening)\nconst stream = Stream.fromIterable([1, 2]).pipe(\n Stream.flatMap((n) => Stream.fromIterable([n, n * 10]))\n); // Stream\n```\n\n**Explanation:** \n`flatMap` lets you build pipelines where each step can depend on the result of the previous one, and the structure is always flattened—no `Option>` or `Effect>`.", - "antiPattern": "Manually unwrapping the value (e.g., with `.getOrElse`, `.unsafeRunSync`, etc.), then creating a new effect/option/either/stream. \nThis breaks composability, loses error/context handling, and leads to deeply nested or unsafe code.", - "explanation": "`flatMap` is the key to sequencing dependent steps in functional programming. \nIt allows you to express workflows where each step may fail, be optional, or produce multiple results, and ensures that errors and context are handled automatically.", - "content": "# Chaining Computations with `flatMap`\n\n## Guideline\n\nUse the `flatMap` combinator to chain together computations where each step may itself return an `Effect`, `Stream`, `Option`, or `Either`. \n`flatMap` ensures that the result is always \"flattened\"—you never get nested types.\n\n## Rationale\n\n`flatMap` is the key to sequencing dependent steps in functional programming. \nIt allows you to express workflows where each step may fail, be optional, or produce multiple results, and ensures that errors and context are handled automatically.\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Chain two effectful computations\nconst effect = Effect.succeed(2).pipe(\n Effect.flatMap((n) => Effect.succeed(n * 10))\n); // Effect\n\n// Option: Chain two optional computations\nconst option = Option.some(2).pipe(Option.flatMap((n) => Option.some(n * 10))); // Option\n\n// Either: Chain two computations that may fail\nconst either = Either.right(2).pipe(\n Either.flatMap((n) => Either.right(n * 10))\n); // Either\n\n// Stream: Chain streams (flattening)\nconst stream = Stream.fromIterable([1, 2]).pipe(\n Stream.flatMap((n) => Stream.fromIterable([n, n * 10]))\n); // Stream\n```\n\n**Explanation:** \n`flatMap` lets you build pipelines where each step can depend on the result of the previous one, and the structure is always flattened—no `Option>` or `Effect>`.\n\n## Anti-Pattern\n\nManually unwrapping the value (e.g., with `.getOrElse`, `.unsafeRunSync`, etc.), then creating a new effect/option/either/stream. \nThis breaks composability, loses error/context handling, and leads to deeply nested or unsafe code." - }, - { - "id": "pattern-option-either-checks", - "title": "Checking Option and Either Cases", - "description": "Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic.", - "skillLevel": "beginner", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Option, Either } from \"effect\";\n\n// Option: Check if value is Some or None\nconst option = Option.some(42);\n\nif (Option.isSome(option)) {\n // option.value is available here\n console.log(\"We have a value:\", option.value);\n} else if (Option.isNone(option)) {\n console.log(\"No value present\");\n}\n\n// Either: Check if value is Right or Left\nconst either = Either.left(\"error\");\n\nif (Either.isRight(either)) {\n // either.right is available here\n console.log(\"Success:\", either.right);\n} else if (Either.isLeft(either)) {\n // either.left is available here\n console.log(\"Failure:\", either.left);\n}\n\n// Filtering a collection of Options\nconst options = [Option.some(1), Option.none(), Option.some(3)];\nconst presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3]\n```\n\n**Explanation:**\n\n- `Option.isSome` and `Option.isNone` let you check for presence or absence.\n- `Either.isRight` and `Either.isLeft` let you check for success or failure.\n- These are especially useful for filtering or quick conditional logic.", - "antiPattern": "Manually checking internal tags or properties (e.g., `option._tag === \"Some\"`), or using unsafe type assertions, which is less safe and less readable than using the provided predicates.", - "explanation": "These predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions.", - "content": "# Checking Option and Either Cases\n\n## Guideline\n\nUse the `isSome`, `isNone`, `isLeft`, and `isRight` predicates to check the case of an `Option` or `Either` for simple, type-safe branching. \nThese are useful when you need to perform quick checks or filter collections based on presence or success.\n\n## Rationale\n\nThese predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions.\n\n## Good Example\n\n```typescript\nimport { Option, Either } from \"effect\";\n\n// Option: Check if value is Some or None\nconst option = Option.some(42);\n\nif (Option.isSome(option)) {\n // option.value is available here\n console.log(\"We have a value:\", option.value);\n} else if (Option.isNone(option)) {\n console.log(\"No value present\");\n}\n\n// Either: Check if value is Right or Left\nconst either = Either.left(\"error\");\n\nif (Either.isRight(either)) {\n // either.right is available here\n console.log(\"Success:\", either.right);\n} else if (Either.isLeft(either)) {\n // either.left is available here\n console.log(\"Failure:\", either.left);\n}\n\n// Filtering a collection of Options\nconst options = [Option.some(1), Option.none(), Option.some(3)];\nconst presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3]\n```\n\n**Explanation:**\n\n- `Option.isSome` and `Option.isNone` let you check for presence or absence.\n- `Either.isRight` and `Either.isLeft` let you check for success or failure.\n- These are especially useful for filtering or quick conditional logic.\n\n## Anti-Pattern\n\nManually checking internal tags or properties (e.g., `option._tag === \"Some\"`), or using unsafe type assertions, which is less safe and less readable than using the provided predicates." - }, - { - "id": "stream-collect-results", - "title": "Collect All Results into a List", - "description": "Use Stream.runCollect to execute a stream and collect all its emitted values into a Chunk.", - "skillLevel": "beginner", - "useCases": [ - "building-data-pipelines" - ], - "example": "This example creates a stream of numbers, filters for only the even ones, transforms them into strings, and then uses `runCollect` to gather the final results into a `Chunk`.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\nconst program = Stream.range(1, 10).pipe(\n // Find all the even numbers\n Stream.filter((n) => n % 2 === 0),\n // Transform them into strings\n Stream.map((n) => `Even number: ${n}`),\n // Run the stream and collect the results\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const results = yield* program;\n yield* Effect.log(\n `Collected results: ${JSON.stringify(Chunk.toArray(results))}`\n );\n return results;\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\nCollected results: [\n 'Even number: 2',\n 'Even number: 4',\n 'Even number: 6',\n 'Even number: 8',\n 'Even number: 10'\n]\n*/\n```", - "antiPattern": "The anti-pattern is using `Stream.runCollect` on a stream that produces an unbounded or extremely large number of items. This will inevitably lead to an out-of-memory error.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// An infinite stream of numbers\nconst infiniteStream = Stream.range(1, Infinity);\n\nconst program = infiniteStream.pipe(\n // This will run forever, attempting to buffer an infinite number of items.\n Stream.runCollect\n);\n\n// This program will never finish and will eventually crash the process\n// by consuming all available memory.\n// Effect.runPromise(program);\nconsole.log(\n \"This code is commented out because it would cause an out-of-memory crash.\"\n);\n```\n\nThis is a critical mistake because `runCollect` must hold every single item emitted by the stream in memory simultaneously. For pipelines that process huge files, infinite data sources, or are designed to run forever, `runCollect` is the wrong tool. In those cases, you should use a sink like `Stream.runDrain`, which processes items without collecting them.", - "explanation": "A \"sink\" is a terminal operator that consumes a stream and produces a final `Effect`. `Stream.runCollect` is the most fundamental sink. It provides the bridge from the lazy, pull-based world of `Stream` back to the familiar world of a single `Effect` that resolves with a standard data structure.\n\nUsing `Stream.runCollect` is essential when:\n\n1. **You Need the Final Result**: The goal of your pipeline is to produce a complete list of transformed items that you need to use in a subsequent step (e.g., to return as a single JSON array from an API).\n2. **Simplicity is Key**: It's the most straightforward way to \"run\" a stream and see its output. It declaratively states your intent: \"execute this entire pipeline and give me all the results.\"\n3. **The Dataset is Bounded**: It's designed for streams where the total number of items is known to be finite and small enough to fit comfortably in memory.\n\nThe result of `Stream.runCollect` is an `Effect` that, when executed, yields a `Chunk` containing all the items emitted by the stream.\n\n---", - "content": "## Guideline\n\nTo execute a stream and collect all of its emitted values into a single, in-memory list, use the `Stream.runCollect` sink.\n\n---\n\n## Rationale\n\nA \"sink\" is a terminal operator that consumes a stream and produces a final `Effect`. `Stream.runCollect` is the most fundamental sink. It provides the bridge from the lazy, pull-based world of `Stream` back to the familiar world of a single `Effect` that resolves with a standard data structure.\n\nUsing `Stream.runCollect` is essential when:\n\n1. **You Need the Final Result**: The goal of your pipeline is to produce a complete list of transformed items that you need to use in a subsequent step (e.g., to return as a single JSON array from an API).\n2. **Simplicity is Key**: It's the most straightforward way to \"run\" a stream and see its output. It declaratively states your intent: \"execute this entire pipeline and give me all the results.\"\n3. **The Dataset is Bounded**: It's designed for streams where the total number of items is known to be finite and small enough to fit comfortably in memory.\n\nThe result of `Stream.runCollect` is an `Effect` that, when executed, yields a `Chunk` containing all the items emitted by the stream.\n\n---\n\n## Good Example\n\nThis example creates a stream of numbers, filters for only the even ones, transforms them into strings, and then uses `runCollect` to gather the final results into a `Chunk`.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\nconst program = Stream.range(1, 10).pipe(\n // Find all the even numbers\n Stream.filter((n) => n % 2 === 0),\n // Transform them into strings\n Stream.map((n) => `Even number: ${n}`),\n // Run the stream and collect the results\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const results = yield* program;\n yield* Effect.log(\n `Collected results: ${JSON.stringify(Chunk.toArray(results))}`\n );\n return results;\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\nCollected results: [\n 'Even number: 2',\n 'Even number: 4',\n 'Even number: 6',\n 'Even number: 8',\n 'Even number: 10'\n]\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is using `Stream.runCollect` on a stream that produces an unbounded or extremely large number of items. This will inevitably lead to an out-of-memory error.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// An infinite stream of numbers\nconst infiniteStream = Stream.range(1, Infinity);\n\nconst program = infiniteStream.pipe(\n // This will run forever, attempting to buffer an infinite number of items.\n Stream.runCollect\n);\n\n// This program will never finish and will eventually crash the process\n// by consuming all available memory.\n// Effect.runPromise(program);\nconsole.log(\n \"This code is commented out because it would cause an out-of-memory crash.\"\n);\n```\n\nThis is a critical mistake because `runCollect` must hold every single item emitted by the stream in memory simultaneously. For pipelines that process huge files, infinite data sources, or are designed to run forever, `runCollect` is the wrong tool. In those cases, you should use a sink like `Stream.runDrain`, which processes items without collecting them." - }, - { - "id": "combinator-zip", - "title": "Combining Values with zip", - "description": "Use zip to run two computations and combine their results into a tuple, preserving error and context handling.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Either, Option, Stream } from \"effect\";\n\n// Effect: Combine two effects and get both results\nconst effectA = Effect.succeed(1);\nconst effectB = Effect.succeed(\"hello\");\nconst zippedEffect = effectA.pipe(Effect.zip(effectB)); // Effect<[number, string]>\n\n// Option: Combine two options, only Some if both are Some\nconst optionA = Option.some(1);\nconst optionB = Option.some(\"hello\");\nconst zippedOption = Option.all([optionA, optionB]); // Option<[number, string]>\n\n// Either: Combine two eithers, only Right if both are Right\nconst eitherA = Either.right(1);\nconst eitherB = Either.right(\"hello\");\nconst zippedEither = Either.all([eitherA, eitherB]); // Either\n\n// Stream: Pair up values from two streams\nconst streamA = Stream.fromIterable([1, 2, 3]);\nconst streamB = Stream.fromIterable([\"a\", \"b\", \"c\"]);\nconst zippedStream = streamA.pipe(Stream.zip(streamB)); // Stream<[number, string]>\n```\n\n**Explanation:** \n`zip` runs both computations and pairs their results. \nIf either computation fails (or is None/Left/empty), the result is a failure (or None/Left/empty).", - "antiPattern": "Manually running two computations, extracting their results, and pairing them outside the combinator world. \nThis breaks composability, loses error/context handling, and can lead to subtle bugs.", - "explanation": "`zip` lets you compose computations that are independent but whose results you want to use together. \nIt preserves error handling and context, and keeps your code declarative and type-safe.", - "content": "# Combining Values with `zip`\n\n## Guideline\n\nUse the `zip` combinator to combine two computations, pairing their results together. \nThis works for `Effect`, `Stream`, `Option`, and `Either`, and is useful when you want to run two computations and work with both results.\n\n## Rationale\n\n`zip` lets you compose computations that are independent but whose results you want to use together. \nIt preserves error handling and context, and keeps your code declarative and type-safe.\n\n## Good Example\n\n```typescript\nimport { Effect, Either, Option, Stream } from \"effect\";\n\n// Effect: Combine two effects and get both results\nconst effectA = Effect.succeed(1);\nconst effectB = Effect.succeed(\"hello\");\nconst zippedEffect = effectA.pipe(Effect.zip(effectB)); // Effect<[number, string]>\n\n// Option: Combine two options, only Some if both are Some\nconst optionA = Option.some(1);\nconst optionB = Option.some(\"hello\");\nconst zippedOption = Option.all([optionA, optionB]); // Option<[number, string]>\n\n// Either: Combine two eithers, only Right if both are Right\nconst eitherA = Either.right(1);\nconst eitherB = Either.right(\"hello\");\nconst zippedEither = Either.all([eitherA, eitherB]); // Either\n\n// Stream: Pair up values from two streams\nconst streamA = Stream.fromIterable([1, 2, 3]);\nconst streamB = Stream.fromIterable([\"a\", \"b\", \"c\"]);\nconst zippedStream = streamA.pipe(Stream.zip(streamB)); // Stream<[number, string]>\n```\n\n**Explanation:** \n`zip` runs both computations and pairs their results. \nIf either computation fails (or is None/Left/empty), the result is a failure (or None/Left/empty).\n\n## Anti-Pattern\n\nManually running two computations, extracting their results, and pairing them outside the combinator world. \nThis breaks composability, loses error/context handling, and can lead to subtle bugs." - }, - { - "id": "data-struct", - "title": "Comparing Data by Value with Data.struct", - "description": "Use Data.struct to define objects whose equality is based on their contents, enabling safe and predictable comparisons.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal objects\nconst user1 = Data.struct({ id: 1, name: \"Alice\" });\nconst user2 = Data.struct({ id: 1, name: \"Alice\" });\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(user1, user2); // true\n\n// Use in a HashSet or as keys in a Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(user1);\nconsole.log(HashSet.has(set, user2)); // true\n```\n\n**Explanation:**\n\n- `Data.struct` creates immutable objects with value-based equality.\n- Use for domain entities, value objects, and when storing objects in sets or as map keys.\n- Avoids bugs from reference-based comparison.", - "antiPattern": "Using plain JavaScript objects for value-based logic, which compares by reference and can lead to incorrect equality checks and collection behavior.", - "explanation": "JavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects. \n`Data.struct` ensures that two objects with the same contents are considered equal, supporting value-based logic and collections.", - "content": "# Comparing Data by Value with `Data.struct`\n\n## Guideline\n\nUse `Data.struct` to create immutable, structurally-typed objects whose equality is based on their contents, not their reference. \nThis enables safe, predictable comparisons and is ideal for domain modeling.\n\n## Rationale\n\nJavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects. \n`Data.struct` ensures that two objects with the same contents are considered equal, supporting value-based logic and collections.\n\n## Good Example\n\n```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal objects\nconst user1 = Data.struct({ id: 1, name: \"Alice\" });\nconst user2 = Data.struct({ id: 1, name: \"Alice\" });\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(user1, user2); // true\n\n// Use in a HashSet or as keys in a Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(user1);\nconsole.log(HashSet.has(set, user2)); // true\n```\n\n**Explanation:**\n\n- `Data.struct` creates immutable objects with value-based equality.\n- Use for domain entities, value objects, and when storing objects in sets or as map keys.\n- Avoids bugs from reference-based comparison.\n\n## Anti-Pattern\n\nUsing plain JavaScript objects for value-based logic, which compares by reference and can lead to incorrect equality checks and collection behavior." - }, - { - "id": "comparing-data-by-value-with-structural-equality", - "title": "Comparing Data by Value with Structural Equality", - "description": "Use Data.struct or implement the Equal interface for value-based comparison of objects and classes.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "We define two points using `Data.struct`. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match.\n\n```typescript\nimport { Data, Equal, Effect } from \"effect\";\n\n// Define a Point type with structural equality\ninterface Point {\n readonly _tag: \"Point\";\n readonly x: number;\n readonly y: number;\n}\n\nconst Point = Data.tagged(\"Point\");\n\n// Create a program to demonstrate structural equality\nconst program = Effect.gen(function* () {\n const p1 = Point({ x: 1, y: 2 });\n const p2 = Point({ x: 1, y: 2 });\n const p3 = Point({ x: 3, y: 4 });\n\n // Standard reference equality fails\n yield* Effect.log(\"Comparing points with reference equality (===):\");\n yield* Effect.log(`p1 === p2: ${p1 === p2}`);\n\n // Structural equality works as expected\n yield* Effect.log(\"\\nComparing points with structural equality:\");\n yield* Effect.log(`p1 equals p2: ${Equal.equals(p1, p2)}`);\n yield* Effect.log(`p1 equals p3: ${Equal.equals(p1, p3)}`);\n\n // Show the actual points\n yield* Effect.log(\"\\nPoint values:\");\n yield* Effect.log(`p1: ${JSON.stringify(p1)}`);\n yield* Effect.log(`p2: ${JSON.stringify(p2)}`);\n yield* Effect.log(`p3: ${JSON.stringify(p3)}`);\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "Relying on `===` for object or array comparison. This will lead to bugs when you expect two objects with the same values to be treated as equal, especially when working with data in collections, `Ref`s, or `Effect`'s success values.\n\n```typescript\n// ❌ WRONG: This will not behave as expected.\nconst user1 = { id: 1, name: \"Paul\" };\nconst user2 = { id: 1, name: \"Paul\" };\n\nif (user1 === user2) {\n // This code block will never be reached.\n console.log(\"Users are the same.\");\n}\n\n// Another common pitfall\nconst selectedUsers = [user1];\n// This check will fail, even though a user with id 1 is in the array.\nif (selectedUsers.includes({ id: 1, name: \"Paul\" })) {\n // ...\n}\n```", - "explanation": "In JavaScript, comparing two non-primitive values with `===` checks for _referential equality_. It only returns `true` if they are the exact same instance in memory. This means two objects with identical contents are not considered equal, which is a common source of bugs.\n\n```typescript\n{ a: 1 } === { a: 1 } // false!\n```\n\nEffect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Either`, `Chunk`, etc.) can be compared by their structure and values. By using helpers like `Data.struct`, you can easily give your own data structures this same powerful and predictable behavior.\n\n---", - "content": "## Guideline\n\nTo compare objects or classes by their contents rather than by their memory reference, use one of two methods:\n\n1. **For plain data objects:** Define them with `Data.struct`.\n2. **For classes:** Extend `Data.Class` or implement the `Equal.Equal` interface.\n\nThen, compare instances using the `Equal.equals(a, b)` function.\n\n---\n\n## Rationale\n\nIn JavaScript, comparing two non-primitive values with `===` checks for _referential equality_. It only returns `true` if they are the exact same instance in memory. This means two objects with identical contents are not considered equal, which is a common source of bugs.\n\n```typescript\n{ a: 1 } === { a: 1 } // false!\n```\n\nEffect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Either`, `Chunk`, etc.) can be compared by their structure and values. By using helpers like `Data.struct`, you can easily give your own data structures this same powerful and predictable behavior.\n\n---\n\n## Good Example\n\nWe define two points using `Data.struct`. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match.\n\n```typescript\nimport { Data, Equal, Effect } from \"effect\";\n\n// Define a Point type with structural equality\ninterface Point {\n readonly _tag: \"Point\";\n readonly x: number;\n readonly y: number;\n}\n\nconst Point = Data.tagged(\"Point\");\n\n// Create a program to demonstrate structural equality\nconst program = Effect.gen(function* () {\n const p1 = Point({ x: 1, y: 2 });\n const p2 = Point({ x: 1, y: 2 });\n const p3 = Point({ x: 3, y: 4 });\n\n // Standard reference equality fails\n yield* Effect.log(\"Comparing points with reference equality (===):\");\n yield* Effect.log(`p1 === p2: ${p1 === p2}`);\n\n // Structural equality works as expected\n yield* Effect.log(\"\\nComparing points with structural equality:\");\n yield* Effect.log(`p1 equals p2: ${Equal.equals(p1, p2)}`);\n yield* Effect.log(`p1 equals p3: ${Equal.equals(p1, p3)}`);\n\n // Show the actual points\n yield* Effect.log(\"\\nPoint values:\");\n yield* Effect.log(`p1: ${JSON.stringify(p1)}`);\n yield* Effect.log(`p2: ${JSON.stringify(p2)}`);\n yield* Effect.log(`p3: ${JSON.stringify(p3)}`);\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nRelying on `===` for object or array comparison. This will lead to bugs when you expect two objects with the same values to be treated as equal, especially when working with data in collections, `Ref`s, or `Effect`'s success values.\n\n```typescript\n// ❌ WRONG: This will not behave as expected.\nconst user1 = { id: 1, name: \"Paul\" };\nconst user2 = { id: 1, name: \"Paul\" };\n\nif (user1 === user2) {\n // This code block will never be reached.\n console.log(\"Users are the same.\");\n}\n\n// Another common pitfall\nconst selectedUsers = [user1];\n// This check will fail, even though a user with id 1 is in the array.\nif (selectedUsers.includes({ id: 1, name: \"Paul\" })) {\n // ...\n}\n```" - }, - { - "id": "api-middleware", - "title": "Compose API Middleware", - "description": "Use Effect composition to build a middleware pipeline that processes requests.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "example": "```typescript\nimport { Effect, Context, Layer, Duration } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define middleware type\n// ============================================\n\ntype Handler = Effect.Effect\n\ntype Middleware = (\n handler: Handler\n) => Handler\n\n// ============================================\n// 2. Logging middleware\n// ============================================\n\nconst withLogging: Middleware =\n (handler) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const startTime = Date.now()\n\n yield* Effect.log(`→ ${request.method} ${request.url}`)\n\n const response = yield* handler\n\n const duration = Date.now() - startTime\n yield* Effect.log(`← ${response.status} (${duration}ms)`)\n\n return response\n })\n\n// ============================================\n// 3. Timing middleware (adds header)\n// ============================================\n\nconst withTiming: Middleware = (handler) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n const response = yield* handler\n const duration = Date.now() - startTime\n\n return HttpServerResponse.setHeader(\n response,\n \"X-Response-Time\",\n `${duration}ms`\n )\n })\n\n// ============================================\n// 4. Error handling middleware\n// ============================================\n\nconst withErrorHandling: Middleware = (handler) =>\n handler.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Unhandled error: ${error}`)\n\n return HttpServerResponse.json(\n { error: \"Internal Server Error\" },\n { status: 500 }\n )\n })\n )\n )\n\n// ============================================\n// 5. Request ID middleware\n// ============================================\n\nclass RequestId extends Context.Tag(\"RequestId\")() {}\n\nconst withRequestId: Middleware = (handler) =>\n Effect.gen(function* () {\n const requestId = crypto.randomUUID()\n\n const response = yield* handler.pipe(\n Effect.provideService(RequestId, requestId)\n )\n\n return HttpServerResponse.setHeader(response, \"X-Request-Id\", requestId)\n })\n\n// ============================================\n// 6. Timeout middleware\n// ============================================\n\nconst withTimeout = (duration: Duration.DurationInput): Middleware =>\n (handler) =>\n handler.pipe(\n Effect.timeout(duration),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.succeed(\n HttpServerResponse.json(\n { error: \"Request timeout\" },\n { status: 504 }\n )\n )\n )\n )\n\n// ============================================\n// 7. CORS middleware (see separate pattern)\n// ============================================\n\nconst withCORS = (origin: string): Middleware => (handler) =>\n Effect.gen(function* () {\n const response = yield* handler\n\n return response.pipe(\n HttpServerResponse.setHeader(\"Access-Control-Allow-Origin\", origin),\n HttpServerResponse.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE\"),\n HttpServerResponse.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n )\n })\n\n// ============================================\n// 8. Compose middleware\n// ============================================\n\nconst applyMiddleware = (handler: Handler) =>\n handler.pipe(\n withLogging,\n withTiming,\n withRequestId,\n withTimeout(\"30 seconds\"),\n withCORS(\"*\"),\n withErrorHandling\n )\n\n// ============================================\n// 9. Usage\n// ============================================\n\nconst myHandler = Effect.gen(function* () {\n const requestId = yield* RequestId\n yield* Effect.log(`Processing request ${requestId}`)\n\n return HttpServerResponse.json({ message: \"Hello!\" })\n})\n\nconst protectedHandler = applyMiddleware(myHandler)\n```", - "antiPattern": "", - "explanation": "Middleware provides separation of concerns:\n\n1. **Reusability** - Write once, apply everywhere\n2. **Composability** - Stack multiple middlewares\n3. **Testability** - Test each middleware in isolation\n4. **Clarity** - Handlers focus on business logic\n\n---", - "content": "## Guideline\n\nBuild middleware as composable Effect functions that wrap handlers, adding cross-cutting concerns like logging, authentication, and error handling.\n\n---\n\n## Rationale\n\nMiddleware provides separation of concerns:\n\n1. **Reusability** - Write once, apply everywhere\n2. **Composability** - Stack multiple middlewares\n3. **Testability** - Test each middleware in isolation\n4. **Clarity** - Handlers focus on business logic\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Context, Layer, Duration } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define middleware type\n// ============================================\n\ntype Handler = Effect.Effect\n\ntype Middleware = (\n handler: Handler\n) => Handler\n\n// ============================================\n// 2. Logging middleware\n// ============================================\n\nconst withLogging: Middleware =\n (handler) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const startTime = Date.now()\n\n yield* Effect.log(`→ ${request.method} ${request.url}`)\n\n const response = yield* handler\n\n const duration = Date.now() - startTime\n yield* Effect.log(`← ${response.status} (${duration}ms)`)\n\n return response\n })\n\n// ============================================\n// 3. Timing middleware (adds header)\n// ============================================\n\nconst withTiming: Middleware = (handler) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n const response = yield* handler\n const duration = Date.now() - startTime\n\n return HttpServerResponse.setHeader(\n response,\n \"X-Response-Time\",\n `${duration}ms`\n )\n })\n\n// ============================================\n// 4. Error handling middleware\n// ============================================\n\nconst withErrorHandling: Middleware = (handler) =>\n handler.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Unhandled error: ${error}`)\n\n return HttpServerResponse.json(\n { error: \"Internal Server Error\" },\n { status: 500 }\n )\n })\n )\n )\n\n// ============================================\n// 5. Request ID middleware\n// ============================================\n\nclass RequestId extends Context.Tag(\"RequestId\")() {}\n\nconst withRequestId: Middleware = (handler) =>\n Effect.gen(function* () {\n const requestId = crypto.randomUUID()\n\n const response = yield* handler.pipe(\n Effect.provideService(RequestId, requestId)\n )\n\n return HttpServerResponse.setHeader(response, \"X-Request-Id\", requestId)\n })\n\n// ============================================\n// 6. Timeout middleware\n// ============================================\n\nconst withTimeout = (duration: Duration.DurationInput): Middleware =>\n (handler) =>\n handler.pipe(\n Effect.timeout(duration),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.succeed(\n HttpServerResponse.json(\n { error: \"Request timeout\" },\n { status: 504 }\n )\n )\n )\n )\n\n// ============================================\n// 7. CORS middleware (see separate pattern)\n// ============================================\n\nconst withCORS = (origin: string): Middleware => (handler) =>\n Effect.gen(function* () {\n const response = yield* handler\n\n return response.pipe(\n HttpServerResponse.setHeader(\"Access-Control-Allow-Origin\", origin),\n HttpServerResponse.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE\"),\n HttpServerResponse.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n )\n })\n\n// ============================================\n// 8. Compose middleware\n// ============================================\n\nconst applyMiddleware = (handler: Handler) =>\n handler.pipe(\n withLogging,\n withTiming,\n withRequestId,\n withTimeout(\"30 seconds\"),\n withCORS(\"*\"),\n withErrorHandling\n )\n\n// ============================================\n// 9. Usage\n// ============================================\n\nconst myHandler = Effect.gen(function* () {\n const requestId = yield* RequestId\n yield* Effect.log(`Processing request ${requestId}`)\n\n return HttpServerResponse.json({ message: \"Hello!\" })\n})\n\nconst protectedHandler = applyMiddleware(myHandler)\n```\n\n## Middleware Order\n\n```\nRequest\n ↓\nLogging (start)\n ↓\nRequest ID\n ↓\nTimeout\n ↓\nCORS\n ↓\nError Handling\n ↓\nHandler\n ↓\nError Handling (catch)\n ↓\nCORS (headers)\n ↓\nTimeout (check)\n ↓\nRequest ID (add header)\n ↓\nLogging (end)\n ↓\nResponse\n```\n\n## Common Middleware\n\n| Middleware | Purpose |\n|------------|---------|\n| Logging | Request/response logging |\n| Timing | Performance measurement |\n| Auth | Token validation |\n| Rate Limit | Abuse prevention |\n| CORS | Cross-origin requests |\n| Error | Global error handling |\n| Compression | Response compression |\n| Cache | Response caching |\n\n## Best Practices\n\n1. **Order matters** - Auth before rate limit, error handling outer\n2. **Keep focused** - One concern per middleware\n3. **Type-safe** - Use Effect types for requirements\n4. **Testable** - Test each middleware independently" - }, - { - "id": "compose-scoped-layers", - "title": "Compose Resource Lifecycles with `Layer.merge`", - "description": "Compose multiple scoped layers using `Layer.merge` or by providing one layer to another.", - "skillLevel": "intermediate", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Layer, Console } from \"effect\";\n\n// --- Service 1: Database ---\ninterface DatabaseOps {\n query: (sql: string) => Effect.Effect;\n}\n\nclass Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n query: (sql: string): Effect.Effect =>\n Effect.sync(() => `db says: ${sql}`),\n }),\n}) {}\n\n// --- Service 2: API Client ---\ninterface ApiClientOps {\n fetch: (path: string) => Effect.Effect;\n}\n\nclass ApiClient extends Effect.Service()(\"ApiClient\", {\n sync: () => ({\n fetch: (path: string): Effect.Effect =>\n Effect.sync(() => `api says: ${path}`),\n }),\n}) {}\n\n// --- Application Layer ---\n// We merge the two independent layers into one.\nconst AppLayer = Layer.merge(Database.Default, ApiClient.Default);\n\n// This program uses both services, unaware of their implementation details.\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n const api = yield* ApiClient;\n\n const dbResult = yield* db.query(\"SELECT *\");\n const apiResult = yield* api.fetch(\"/users\");\n\n yield* Effect.log(dbResult);\n yield* Effect.log(apiResult);\n});\n\n// Provide the combined layer to the program.\nEffect.runPromise(Effect.provide(program, AppLayer));\n\n/*\nOutput (note the LIFO release order):\nDatabase pool opened\nAPI client session started\ndb says: SELECT *\napi says: /users\nAPI client session ended\nDatabase pool closed\n*/\n```\n\n**Explanation:**\nWe 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.", - "antiPattern": "A manual, imperative startup and shutdown script. This approach is brittle and error-prone. The developer is responsible for maintaining the correct order of initialization and, more importantly, the reverse order for shutdown. This becomes unmanageable as an application grows.\n\n```typescript\n// ANTI-PATTERN: Manual, brittle, and error-prone\nasync function main() {\n const db = await initDb(); // acquire 1\n const client = await initApiClient(); // acquire 2\n\n try {\n await doWork(db, client); // use\n } finally {\n // This order is easy to get wrong!\n await client.close(); // release 2\n await db.close(); // release 1\n }\n}\n```", - "explanation": "This pattern is the ultimate payoff for defining services with `Layer`. It allows for true modularity. Each service can be defined in its own file, declaring its own resource requirements in its `Live` layer, completely unaware of other services.\n\nWhen you assemble the final application layer, Effect analyzes the dependencies:\n\n1. **Acquisition Order:** It ensures resources are acquired in the correct order. For example, a `Logger` layer might be initialized before a `Database` layer that uses it for logging.\n2. **Release Order:** It guarantees that resources are released in the **exact reverse order** of their acquisition. This is critical for preventing shutdown errors, such as a `UserRepository` trying to log a final message after the `Logger` has already been shut down.\n\nThis automates one of the most complex and error-prone parts of application architecture.", - "content": "# Compose Resource Lifecycles with `Layer.merge`\n\n## Guideline\n\nCombine multiple resource-managing `Layer`s into a single application layer using functions like `Layer.merge`. The Effect runtime will automatically build a dependency graph, acquire resources in the correct order, and release them in the reverse order.\n\n## Rationale\n\nThis pattern is the ultimate payoff for defining services with `Layer`. It allows for true modularity. Each service can be defined in its own file, declaring its own resource requirements in its `Live` layer, completely unaware of other services.\n\nWhen you assemble the final application layer, Effect analyzes the dependencies:\n\n1. **Acquisition Order:** It ensures resources are acquired in the correct order. For example, a `Logger` layer might be initialized before a `Database` layer that uses it for logging.\n2. **Release Order:** It guarantees that resources are released in the **exact reverse order** of their acquisition. This is critical for preventing shutdown errors, such as a `UserRepository` trying to log a final message after the `Logger` has already been shut down.\n\nThis automates one of the most complex and error-prone parts of application architecture.\n\n## Good Example\n\n```typescript\nimport { Effect, Layer, Console } from \"effect\";\n\n// --- Service 1: Database ---\ninterface DatabaseOps {\n query: (sql: string) => Effect.Effect;\n}\n\nclass Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n query: (sql: string): Effect.Effect =>\n Effect.sync(() => `db says: ${sql}`),\n }),\n}) {}\n\n// --- Service 2: API Client ---\ninterface ApiClientOps {\n fetch: (path: string) => Effect.Effect;\n}\n\nclass ApiClient extends Effect.Service()(\"ApiClient\", {\n sync: () => ({\n fetch: (path: string): Effect.Effect =>\n Effect.sync(() => `api says: ${path}`),\n }),\n}) {}\n\n// --- Application Layer ---\n// We merge the two independent layers into one.\nconst AppLayer = Layer.merge(Database.Default, ApiClient.Default);\n\n// This program uses both services, unaware of their implementation details.\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n const api = yield* ApiClient;\n\n const dbResult = yield* db.query(\"SELECT *\");\n const apiResult = yield* api.fetch(\"/users\");\n\n yield* Effect.log(dbResult);\n yield* Effect.log(apiResult);\n});\n\n// Provide the combined layer to the program.\nEffect.runPromise(Effect.provide(program, AppLayer));\n\n/*\nOutput (note the LIFO release order):\nDatabase pool opened\nAPI client session started\ndb says: SELECT *\napi says: /users\nAPI client session ended\nDatabase pool closed\n*/\n```\n\n**Explanation:**\nWe 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.\n\n## Anti-Pattern\n\nA manual, imperative startup and shutdown script. This approach is brittle and error-prone. The developer is responsible for maintaining the correct order of initialization and, more importantly, the reverse order for shutdown. This becomes unmanageable as an application grows.\n\n```typescript\n// ANTI-PATTERN: Manual, brittle, and error-prone\nasync function main() {\n const db = await initDb(); // acquire 1\n const client = await initApiClient(); // acquire 2\n\n try {\n await doWork(db, client); // use\n } finally {\n // This order is easy to get wrong!\n await client.close(); // release 2\n await db.close(); // release 1\n }\n}\n```" - }, - { - "id": "concurrency-pattern-coordinate-with-deferred", - "title": "Concurrency Pattern 1: Coordinate Async Operations with Deferred", - "description": "Use Deferred for one-time async coordination between fibers, enabling multiple consumers to wait for a single producer's result.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates a service startup pattern where multiple workers wait for initialization to complete before starting processing.\n\n```typescript\nimport { Effect, Deferred, Fiber } from \"effect\";\n\ninterface ServiceConfig {\n readonly name: string;\n readonly port: number;\n}\n\ninterface Service {\n readonly name: string;\n readonly isReady: Deferred.Deferred;\n}\n\n// Simulate a service that takes time to initialize\nconst createService = (config: ServiceConfig): Effect.Effect =>\n Effect.gen(function* () {\n const isReady = yield* Deferred.make();\n\n return { name: config.name, isReady };\n });\n\n// Initialize the service (runs in background)\nconst initializeService = (service: Service): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[${service.name}] Starting initialization...`);\n\n // Simulate initialization work\n yield* Effect.sleep(\"1 second\");\n\n yield* Effect.log(`[${service.name}] Initialization complete`);\n\n // Signal that service is ready\n yield* Deferred.succeed(service.isReady, undefined);\n });\n\n// A worker that waits for service to be ready before starting\nconst createWorker = (\n id: number,\n services: Service[]\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[Worker ${id}] Starting, waiting for services...`);\n\n // Wait for all services to be ready\n yield* Effect.all(\n services.map((service) =>\n Deferred.await(service.isReady).pipe(\n Effect.tapError((error) =>\n Effect.log(\n `[Worker ${id}] Error waiting for ${service.name}: ${error}`\n )\n )\n )\n )\n );\n\n yield* Effect.log(`[Worker ${id}] All services ready, starting work`);\n\n // Simulate worker processing\n for (let i = 0; i < 3; i++) {\n yield* Effect.sleep(\"500 millis\");\n yield* Effect.log(`[Worker ${id}] Processing task ${i + 1}`);\n }\n\n yield* Effect.log(`[Worker ${id}] Complete`);\n });\n\n// Main program\nconst program = Effect.gen(function* () {\n // Create services\n const apiService = yield* createService({ name: \"API\", port: 3000 });\n const dbService = yield* createService({ name: \"Database\", port: 5432 });\n const cacheService = yield* createService({ name: \"Cache\", port: 6379 });\n\n const services = [apiService, dbService, cacheService];\n\n // Start initializing services in background\n const initFibers = yield* Effect.all(\n services.map((service) => initializeService(service).pipe(Effect.fork))\n );\n\n // Start workers that wait for services\n const workerFibers = yield* Effect.all(\n [1, 2, 3].map((id) => createWorker(id, services).pipe(Effect.fork))\n );\n\n // Wait for all workers to complete\n yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber)));\n\n // Cancel initialization fibers (they're done anyway)\n yield* Effect.all(initFibers.map((fiber) => Fiber.interrupt(fiber)));\n\n yield* Effect.log(`\\n[MAIN] All workers completed`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates Deferred instances** for each service's readiness\n2. **Starts initialization** in background fibers\n3. **Workers wait** for all services via `Deferred.await`\n4. **Service signals completion** via `Deferred.succeed`\n5. **Workers resume** when all dependencies ready\n\n---", - "antiPattern": "", - "explanation": "Many concurrent systems need to coordinate on events:\n\n- **Service initialization**: Wait for all services to start before accepting requests\n- **Data availability**: Wait for initial data load before processing\n- **External events**: Wait for webhook, signal, or message\n- **Startup gates**: All workers wait for leader to signal start\n\nWithout Deferred:\n\n- Polling wastes CPU (check repeatedly)\n- Callbacks become complex (multiple consumers)\n- No clean semantics for \"wait for this one thing\"\n- Error propagation unclear\n\nWith Deferred:\n\n- Non-blocking wait (fiber suspends)\n- One fiber produces, many consume\n- Clear completion or failure\n- Efficient wakeup when ready\n\n---", - "content": "## Guideline\n\nWhen you need multiple fibers to wait for a single async event (e.g., service initialization, data availability, external signal), use `Deferred`. A Deferred is a one-shot promise that exactly one fiber completes, and many fibers can wait for. This avoids polling and provides clean async signaling.\n\n---\n\n## Rationale\n\nMany concurrent systems need to coordinate on events:\n\n- **Service initialization**: Wait for all services to start before accepting requests\n- **Data availability**: Wait for initial data load before processing\n- **External events**: Wait for webhook, signal, or message\n- **Startup gates**: All workers wait for leader to signal start\n\nWithout Deferred:\n\n- Polling wastes CPU (check repeatedly)\n- Callbacks become complex (multiple consumers)\n- No clean semantics for \"wait for this one thing\"\n- Error propagation unclear\n\nWith Deferred:\n\n- Non-blocking wait (fiber suspends)\n- One fiber produces, many consume\n- Clear completion or failure\n- Efficient wakeup when ready\n\n---\n\n## Good Example\n\nThis example demonstrates a service startup pattern where multiple workers wait for initialization to complete before starting processing.\n\n```typescript\nimport { Effect, Deferred, Fiber } from \"effect\";\n\ninterface ServiceConfig {\n readonly name: string;\n readonly port: number;\n}\n\ninterface Service {\n readonly name: string;\n readonly isReady: Deferred.Deferred;\n}\n\n// Simulate a service that takes time to initialize\nconst createService = (config: ServiceConfig): Effect.Effect =>\n Effect.gen(function* () {\n const isReady = yield* Deferred.make();\n\n return { name: config.name, isReady };\n });\n\n// Initialize the service (runs in background)\nconst initializeService = (service: Service): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[${service.name}] Starting initialization...`);\n\n // Simulate initialization work\n yield* Effect.sleep(\"1 second\");\n\n yield* Effect.log(`[${service.name}] Initialization complete`);\n\n // Signal that service is ready\n yield* Deferred.succeed(service.isReady, undefined);\n });\n\n// A worker that waits for service to be ready before starting\nconst createWorker = (\n id: number,\n services: Service[]\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[Worker ${id}] Starting, waiting for services...`);\n\n // Wait for all services to be ready\n yield* Effect.all(\n services.map((service) =>\n Deferred.await(service.isReady).pipe(\n Effect.tapError((error) =>\n Effect.log(\n `[Worker ${id}] Error waiting for ${service.name}: ${error}`\n )\n )\n )\n )\n );\n\n yield* Effect.log(`[Worker ${id}] All services ready, starting work`);\n\n // Simulate worker processing\n for (let i = 0; i < 3; i++) {\n yield* Effect.sleep(\"500 millis\");\n yield* Effect.log(`[Worker ${id}] Processing task ${i + 1}`);\n }\n\n yield* Effect.log(`[Worker ${id}] Complete`);\n });\n\n// Main program\nconst program = Effect.gen(function* () {\n // Create services\n const apiService = yield* createService({ name: \"API\", port: 3000 });\n const dbService = yield* createService({ name: \"Database\", port: 5432 });\n const cacheService = yield* createService({ name: \"Cache\", port: 6379 });\n\n const services = [apiService, dbService, cacheService];\n\n // Start initializing services in background\n const initFibers = yield* Effect.all(\n services.map((service) => initializeService(service).pipe(Effect.fork))\n );\n\n // Start workers that wait for services\n const workerFibers = yield* Effect.all(\n [1, 2, 3].map((id) => createWorker(id, services).pipe(Effect.fork))\n );\n\n // Wait for all workers to complete\n yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber)));\n\n // Cancel initialization fibers (they're done anyway)\n yield* Effect.all(initFibers.map((fiber) => Fiber.interrupt(fiber)));\n\n yield* Effect.log(`\\n[MAIN] All workers completed`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates Deferred instances** for each service's readiness\n2. **Starts initialization** in background fibers\n3. **Workers wait** for all services via `Deferred.await`\n4. **Service signals completion** via `Deferred.succeed`\n5. **Workers resume** when all dependencies ready\n\n---\n\n## Advanced: Deferred with Timeout and Fallback\n\nHandle cases where initialization takes too long:\n\n```typescript\nconst initializeServiceWithTimeout = (\n service: Service,\n timeoutMs: number = 5000\n): Effect.Effect =>\n Effect.gen(function* () {\n const timeoutDeferred = yield* Deferred.make<\"timeout\">();\n const timeoutFiber = yield* Effect.sleep(`${timeoutMs} millis`).pipe(\n Effect.andThen(() => Deferred.succeed(timeoutDeferred, \"timeout\" as const)),\n Effect.fork\n );\n\n const result = yield* Effect.race(\n initializeService(service),\n Deferred.await(timeoutDeferred)\n );\n\n yield* timeoutFiber.interrupt();\n\n if (result === \"timeout\") {\n yield* Effect.fail(\n new Error(`${service.name} initialization timed out after ${timeoutMs}ms`)\n );\n }\n });\n\n// Worker with graceful degradation\nconst createWorkerWithFallback = (\n id: number,\n services: Service[]\n): Effect.Effect =>\n Effect.gen(function* () {\n const readyServices = yield* Effect.all(\n services.map((service) =>\n Deferred.await(service.isReady).pipe(\n Effect.map(() => service.name),\n Effect.catchAll(() => Effect.succeed(\"unavailable\"))\n )\n )\n );\n\n const available = readyServices.filter((s) => s !== \"unavailable\");\n yield* Effect.log(\n `[Worker ${id}] Ready services: ${available.join(\", \")}`\n );\n\n if (available.length === 0) {\n yield* Effect.log(`[Worker ${id}] No services available, degrading`);\n return;\n }\n\n yield* Effect.log(`[Worker ${id}] Operating with ${available.length} services`);\n });\n```\n\n---\n\n## Advanced: Deferred with Error Propagation\n\nHandle initialization failures that all workers should know about:\n\n```typescript\nclass InitializationError extends Error {\n constructor(readonly service: string, readonly cause: unknown) {\n super(`Failed to initialize ${service}`);\n }\n}\n\nconst initializeServiceWithErrorHandling = (\n service: Service\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[${service.name}] Starting...`);\n\n const result = yield* Effect.gen(function* () {\n try {\n // Simulate potential initialization failure (10% chance)\n if (Math.random() < 0.1) {\n throw new Error(\"Connection refused\");\n }\n\n yield* Effect.sleep(\"1 second\");\n return { success: true as const };\n } catch (error) {\n return { success: false as const, error };\n }\n }).pipe(Effect.either);\n\n if (result._tag === \"Right\" && result.right.success) {\n yield* Effect.log(`[${service.name}] Ready`);\n yield* Deferred.succeed(service.isReady, undefined);\n } else {\n const error = new InitializationError(\n service.name,\n result._tag === \"Left\" ? result.left : result.right.error\n );\n\n yield* Effect.log(\n `[${service.name}] Failed: ${error.message}`\n );\n\n // Fail the deferred so all waiters know about the error\n yield* Deferred.fail(service.isReady, error);\n }\n });\n\n// Worker handles initialization failures\nconst createWorkerWithErrorHandling = (\n id: number,\n services: Service[]\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[Worker ${id}] Waiting for services...`);\n\n const result = yield* Effect.all(\n services.map((service) => Deferred.await(service.isReady))\n ).pipe(Effect.either);\n\n if (result._tag === \"Left\") {\n yield* Effect.log(`[Worker ${id}] Service initialization failed: ${result.left.message}`);\n yield* Effect.fail(result.left);\n }\n\n yield* Effect.log(`[Worker ${id}] All services ready`);\n });\n```\n\n---\n\n## Advanced: Multiple Stages with Chained Deferreds\n\nCoordinate multi-stage startup where each stage depends on the previous:\n\n```typescript\ninterface StartupStage {\n readonly name: string;\n readonly ready: Deferred.Deferred;\n}\n\nconst createStagedStartup = (): Effect.Effect =>\n Effect.gen(function* () {\n const stages: StartupStage[] = [\n { name: \"config-load\", ready: yield* Deferred.make() },\n { name: \"database-connect\", ready: yield* Deferred.make() },\n { name: \"services-start\", ready: yield* Deferred.make() },\n { name: \"ready-to-serve\", ready: yield* Deferred.make() },\n ];\n\n // Stage 1: Load config\n yield* Effect.gen(function* () {\n yield* Effect.log(`[Stage 1] Loading config...`);\n yield* Effect.sleep(\"500 millis\");\n yield* Effect.log(`[Stage 1] Config loaded`);\n yield* Deferred.succeed(stages[0].ready, undefined);\n }).pipe(Effect.fork);\n\n // Stage 2: Wait for config, then connect database\n yield* Effect.gen(function* () {\n yield* Deferred.await(stages[0].ready);\n yield* Effect.log(`[Stage 2] Connecting to database...`);\n yield* Effect.sleep(\"1 second\");\n yield* Effect.log(`[Stage 2] Database connected`);\n yield* Deferred.succeed(stages[1].ready, undefined);\n }).pipe(Effect.fork);\n\n // Stage 3: Wait for database, then start services\n yield* Effect.gen(function* () {\n yield* Deferred.await(stages[1].ready);\n yield* Effect.log(`[Stage 3] Starting services...`);\n yield* Effect.sleep(\"500 millis\");\n yield* Effect.log(`[Stage 3] Services started`);\n yield* Deferred.succeed(stages[2].ready, undefined);\n }).pipe(Effect.fork);\n\n // Stage 4: Wait for services, then ready to serve\n yield* Effect.gen(function* () {\n yield* Deferred.await(stages[2].ready);\n yield* Effect.log(`[Stage 4] Ready to serve!`);\n yield* Deferred.succeed(stages[3].ready, undefined);\n }).pipe(Effect.fork);\n\n // Wait for final stage\n yield* Deferred.await(stages[3].ready);\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Deferred when:**\n\n- Multiple fibers wait for a single async event\n- One-time initialization or startup gates\n- Producer signals completion to many consumers\n- Need efficient async signaling (no polling)\n- Coordinating service dependencies\n\n⚠️ **Trade-offs:**\n\n- One-shot only (can't reset a Deferred)\n- For repeated signals use other primitives (Queue, PubSub)\n- Error affects all waiters (careful propagation)\n\n---\n\n## See Also\n\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background fiber execution\n- [Understand Fibers as Lightweight Threads](./understand-fibers-as-lightweight-threads.mdx) - Fiber concurrency\n- [Race Concurrent Effects](./race-concurrent-effects.mdx) - Racing with timeouts\n- [Decouple Fibers with Queue/PubSub](./decouple-fibers-with-queue-pubsub.mdx) - Repeated signaling" - }, - { - "id": "concurrency-pattern-rate-limit-with-semaphore", - "title": "Concurrency Pattern 2: Rate Limit Concurrent Access with Semaphore", - "description": "Use Semaphore to limit concurrent access to resources, preventing overload and enabling fair resource distribution.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates limiting concurrent database connections using a Semaphore, preventing connection pool exhaustion.\n\n```typescript\nimport { Effect, Semaphore, Fiber } from \"effect\";\n\ninterface QueryResult {\n readonly id: number;\n readonly result: string;\n readonly duration: number;\n}\n\n// Simulate a database query that holds a connection\nconst executeQuery = (\n queryId: number,\n connectionId: number,\n durationMs: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n\n yield* Effect.log(\n `[Query ${queryId}] Using connection ${connectionId}, duration: ${durationMs}ms`\n );\n\n // Simulate query execution\n yield* Effect.sleep(`${durationMs} millis`);\n\n const duration = Date.now() - startTime;\n\n return {\n id: queryId,\n result: `Result from query ${queryId}`,\n duration,\n };\n });\n\n// Pool configuration\ninterface ConnectionPoolConfig {\n readonly maxConnections: number;\n readonly queryTimeout?: number;\n}\n\n// Create a rate-limited query executor\nconst createRateLimitedQueryExecutor = (\n config: ConnectionPoolConfig\n): Effect.Effect<\n (queryId: number, durationMs: number) => Effect.Effect\n> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(config.maxConnections);\n let connectionCounter = 0;\n\n return (queryId: number, durationMs: number) =>\n Effect.gen(function* () {\n // Acquire a permit (wait if none available)\n yield* Semaphore.acquire(semaphore);\n\n const connectionId = ++connectionCounter;\n\n // Use try-finally to ensure permit is released\n const result = yield* executeQuery(queryId, connectionId, durationMs).pipe(\n Effect.ensuring(\n Semaphore.release(semaphore).pipe(\n Effect.tap(() =>\n Effect.log(`[Query ${queryId}] Released connection ${connectionId}`)\n )\n )\n )\n );\n\n return result;\n });\n });\n\n// Simulate multiple queries arriving\nconst program = Effect.gen(function* () {\n const executor = yield* createRateLimitedQueryExecutor({\n maxConnections: 3, // Only 3 concurrent connections\n });\n\n // Generate 10 queries with varying durations\n const queries = Array.from({ length: 10 }, (_, i) => ({\n id: i + 1,\n duration: 500 + Math.random() * 1500, // 500-2000ms\n }));\n\n console.log(`\\n[POOL] Starting with max 3 concurrent connections\\n`);\n\n // Execute all queries with concurrency limit\n const results = yield* Effect.all(\n queries.map((q) =>\n executor(q.id, Math.round(q.duration)).pipe(Effect.fork)\n )\n ).pipe(\n Effect.andThen((fibers) =>\n Effect.all(fibers.map((fiber) => Fiber.join(fiber)))\n )\n );\n\n console.log(`\\n[POOL] All queries completed\\n`);\n\n // Summary\n const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);\n const avgDuration = totalDuration / results.length;\n\n console.log(`[SUMMARY]`);\n console.log(` Total queries: ${results.length}`);\n console.log(` Avg duration: ${Math.round(avgDuration)}ms`);\n console.log(` Total time: ${Math.max(...results.map((r) => r.duration))}ms (parallel)`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates a Semaphore** with fixed permit count\n2. **Acquires permit** before using connection\n3. **Executes operation** while holding permit\n4. **Releases permit** in finally block (guaranteed)\n5. **Fair queuing** of waiting queries\n\n---", - "antiPattern": "", - "explanation": "Resource constraints require limiting concurrency:\n\n- **Connection pools**: Database limited to N connections\n- **API rate limits**: Service allows only M requests per second\n- **Memory limits**: Large operations can't all run simultaneously\n- **CPU constraints**: Too many threads waste cycles on context switching\n- **Backpressure**: Prevent downstream from being overwhelmed\n\nWithout Semaphore:\n\n- All operations run simultaneously, exhausting resources\n- Connection pool overflows, requests fail\n- Memory pressure causes garbage collection pauses\n- No fair ordering (first-come-first-served)\n\nWith Semaphore:\n\n- Fixed concurrency limit\n- Fair queuing of waiting operations\n- Backpressure naturally flows upstream\n- Clear ownership of permits\n\n---", - "content": "## Guideline\n\nWhen you need to limit how many operations can run concurrently (e.g., max 10 database connections, max 5 API calls per second), use `Semaphore`. A Semaphore tracks a pool of permits; operations acquire a permit before proceeding and release it when done. Waiting operations are queued fairly.\n\n---\n\n## Rationale\n\nResource constraints require limiting concurrency:\n\n- **Connection pools**: Database limited to N connections\n- **API rate limits**: Service allows only M requests per second\n- **Memory limits**: Large operations can't all run simultaneously\n- **CPU constraints**: Too many threads waste cycles on context switching\n- **Backpressure**: Prevent downstream from being overwhelmed\n\nWithout Semaphore:\n\n- All operations run simultaneously, exhausting resources\n- Connection pool overflows, requests fail\n- Memory pressure causes garbage collection pauses\n- No fair ordering (first-come-first-served)\n\nWith Semaphore:\n\n- Fixed concurrency limit\n- Fair queuing of waiting operations\n- Backpressure naturally flows upstream\n- Clear ownership of permits\n\n---\n\n## Good Example\n\nThis example demonstrates limiting concurrent database connections using a Semaphore, preventing connection pool exhaustion.\n\n```typescript\nimport { Effect, Semaphore, Fiber } from \"effect\";\n\ninterface QueryResult {\n readonly id: number;\n readonly result: string;\n readonly duration: number;\n}\n\n// Simulate a database query that holds a connection\nconst executeQuery = (\n queryId: number,\n connectionId: number,\n durationMs: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n\n yield* Effect.log(\n `[Query ${queryId}] Using connection ${connectionId}, duration: ${durationMs}ms`\n );\n\n // Simulate query execution\n yield* Effect.sleep(`${durationMs} millis`);\n\n const duration = Date.now() - startTime;\n\n return {\n id: queryId,\n result: `Result from query ${queryId}`,\n duration,\n };\n });\n\n// Pool configuration\ninterface ConnectionPoolConfig {\n readonly maxConnections: number;\n readonly queryTimeout?: number;\n}\n\n// Create a rate-limited query executor\nconst createRateLimitedQueryExecutor = (\n config: ConnectionPoolConfig\n): Effect.Effect<\n (queryId: number, durationMs: number) => Effect.Effect\n> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(config.maxConnections);\n let connectionCounter = 0;\n\n return (queryId: number, durationMs: number) =>\n Effect.gen(function* () {\n // Acquire a permit (wait if none available)\n yield* Semaphore.acquire(semaphore);\n\n const connectionId = ++connectionCounter;\n\n // Use try-finally to ensure permit is released\n const result = yield* executeQuery(queryId, connectionId, durationMs).pipe(\n Effect.ensuring(\n Semaphore.release(semaphore).pipe(\n Effect.tap(() =>\n Effect.log(`[Query ${queryId}] Released connection ${connectionId}`)\n )\n )\n )\n );\n\n return result;\n });\n });\n\n// Simulate multiple queries arriving\nconst program = Effect.gen(function* () {\n const executor = yield* createRateLimitedQueryExecutor({\n maxConnections: 3, // Only 3 concurrent connections\n });\n\n // Generate 10 queries with varying durations\n const queries = Array.from({ length: 10 }, (_, i) => ({\n id: i + 1,\n duration: 500 + Math.random() * 1500, // 500-2000ms\n }));\n\n console.log(`\\n[POOL] Starting with max 3 concurrent connections\\n`);\n\n // Execute all queries with concurrency limit\n const results = yield* Effect.all(\n queries.map((q) =>\n executor(q.id, Math.round(q.duration)).pipe(Effect.fork)\n )\n ).pipe(\n Effect.andThen((fibers) =>\n Effect.all(fibers.map((fiber) => Fiber.join(fiber)))\n )\n );\n\n console.log(`\\n[POOL] All queries completed\\n`);\n\n // Summary\n const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);\n const avgDuration = totalDuration / results.length;\n\n console.log(`[SUMMARY]`);\n console.log(` Total queries: ${results.length}`);\n console.log(` Avg duration: ${Math.round(avgDuration)}ms`);\n console.log(` Total time: ${Math.max(...results.map((r) => r.duration))}ms (parallel)`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates a Semaphore** with fixed permit count\n2. **Acquires permit** before using connection\n3. **Executes operation** while holding permit\n4. **Releases permit** in finally block (guaranteed)\n5. **Fair queuing** of waiting queries\n\n---\n\n## Advanced: Semaphore with Timeout\n\nHandle cases where permits aren't available within deadline:\n\n```typescript\nconst createTimedRateLimitedExecutor = (\n config: ConnectionPoolConfig & { readonly acquireTimeoutMs: number }\n): Effect.Effect<\n (queryId: number, durationMs: number) => Effect.Effect\n> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(config.maxConnections);\n let connectionCounter = 0;\n\n return (queryId: number, durationMs: number) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[Query ${queryId}] Attempting to acquire connection (timeout: ${config.acquireTimeoutMs}ms)`\n );\n\n // Try to acquire with timeout\n const acquired = yield* Semaphore.acquire(semaphore).pipe(\n Effect.timeout(`${config.acquireTimeoutMs} millis`),\n Effect.either\n );\n\n if (acquired._tag === \"Left\") {\n yield* Effect.log(\n `[Query ${queryId}] ❌ No connection available within timeout`\n );\n yield* Effect.fail(\n new Error(\n `Failed to acquire connection for query ${queryId} within ${config.acquireTimeoutMs}ms`\n )\n );\n }\n\n const connectionId = ++connectionCounter;\n\n return yield* executeQuery(queryId, connectionId, durationMs).pipe(\n Effect.ensuring(Semaphore.release(semaphore))\n );\n });\n });\n```\n\n---\n\n## Advanced: Priority Semaphore\n\nGive priority to certain queries:\n\n```typescript\ninterface PrioritizedQuery {\n readonly queryId: number;\n readonly duration: number;\n readonly priority: number; // Higher = more important\n}\n\nconst createPrioritySemaphore = (\n maxPermits: number\n): Effect.Effect<{\n acquire: (priority: number) => Effect.Effect;\n release: () => Effect.Effect;\n}> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(maxPermits);\n const waitingQueue: Array<{\n priority: number;\n resolve: () => void;\n }> = [];\n\n return {\n acquire: (priority: number) =>\n Effect.gen(function* () {\n // Try immediate acquisition\n const available = yield* semaphore.pipe(\n Effect.flatMap(() => Effect.succeed(true)),\n Effect.catchAll(() => Effect.succeed(false))\n );\n\n if (available) {\n yield* Semaphore.acquire(semaphore);\n return;\n }\n\n // Add to priority queue\n yield* new Promise((resolve) => {\n waitingQueue.push({ priority, resolve });\n waitingQueue.sort((a, b) => b.priority - a.priority);\n\n // Check if we can dequeue\n setImmediate(() => {\n const next = waitingQueue.shift();\n if (next) {\n next.resolve();\n }\n });\n });\n }),\n\n release: () =>\n Effect.gen(function* () {\n yield* Semaphore.release(semaphore);\n\n // Dequeue next waiting operation\n const next = waitingQueue.shift();\n if (next) {\n next.resolve();\n }\n }),\n };\n });\n```\n\n---\n\n## Advanced: Adaptive Concurrency Based on Success Rate\n\nAdjust permit count based on failure rates:\n\n```typescript\ninterface AdaptiveConfig extends ConnectionPoolConfig {\n readonly initialConnections: number;\n readonly minConnections: number;\n readonly maxConnectionsAllowed: number;\n readonly failureThreshold: number;\n}\n\nconst createAdaptiveSemaphore = (\n config: AdaptiveConfig\n): Effect.Effect<{\n acquire: () => Effect.Effect;\n release: (success: boolean) => Effect.Effect;\n}> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(config.initialConnections);\n let totalOperations = 0;\n let failedOperations = 0;\n let currentPermits = config.initialConnections;\n\n return {\n acquire: () => Semaphore.acquire(semaphore),\n\n release: (success: boolean) =>\n Effect.gen(function* () {\n totalOperations++;\n if (!success) failedOperations++;\n\n yield* Semaphore.release(semaphore);\n\n // Adjust permits every 100 operations\n if (totalOperations % 100 === 0) {\n const failureRate = failedOperations / totalOperations;\n\n if (failureRate > config.failureThreshold) {\n // Too many failures, reduce concurrency\n if (currentPermits > config.minConnections) {\n currentPermits--;\n yield* Effect.log(\n `[ADAPTIVE] High failure rate (${(failureRate * 100).toFixed(1)}%), reducing to ${currentPermits} permits`\n );\n }\n } else if (failureRate < config.failureThreshold * 0.5) {\n // Low failure rate, can increase concurrency\n if (currentPermits < config.maxConnectionsAllowed) {\n currentPermits++;\n yield* Effect.log(\n `[ADAPTIVE] Low failure rate (${(failureRate * 100).toFixed(1)}%), increasing to ${currentPermits} permits`\n );\n }\n }\n\n // Reset counters\n totalOperations = 0;\n failedOperations = 0;\n }\n }),\n };\n });\n```\n\n---\n\n## Advanced: Metrics and Monitoring\n\nTrack semaphore usage for observability:\n\n```typescript\ninterface SemaphoreMetrics {\n readonly acquiredCount: number;\n readonly waitingCount: number;\n readonly totalAcquisitions: number;\n readonly totalWaitTime: number;\n readonly avgWaitTime: number;\n}\n\nconst createMonitoredSemaphore = (\n maxPermits: number\n): Effect.Effect<{\n acquire: () => Effect.Effect;\n release: () => Effect.Effect;\n metrics: () => SemaphoreMetrics;\n}> =>\n Effect.gen(function* () {\n const semaphore = yield* Semaphore.make(maxPermits);\n const metrics = {\n acquiredCount: 0,\n waitingCount: 0,\n totalAcquisitions: 0,\n totalWaitTime: 0,\n avgWaitTime: 0,\n };\n\n return {\n acquire: () =>\n Effect.gen(function* () {\n const startWait = Date.now();\n metrics.waitingCount++;\n\n yield* Semaphore.acquire(semaphore);\n\n const waitTime = Date.now() - startWait;\n metrics.acquiredCount++;\n metrics.waitingCount--;\n metrics.totalAcquisitions++;\n metrics.totalWaitTime += waitTime;\n metrics.avgWaitTime =\n metrics.totalWaitTime / metrics.totalAcquisitions;\n }),\n\n release: () =>\n Effect.gen(function* () {\n yield* Semaphore.release(semaphore);\n metrics.acquiredCount--;\n }),\n\n metrics: () => ({ ...metrics }),\n };\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Semaphore when:**\n\n- Need to limit concurrent resource usage (connections, threads)\n- Implementing connection pools\n- Rate limiting API calls or requests\n- Preventing resource exhaustion\n- Coordinating fair access to limited resources\n- Need FIFO or priority-based queuing\n\n⚠️ **Trade-offs:**\n\n- Waiting fibers are queued (adds latency)\n- Must remember to release permits (use try-finally)\n- Tuning permit count requires experimentation\n- Doesn't prevent thundering herd on availability\n\n---\n\n## See Also\n\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background fiber execution\n- [Process Collection in Parallel with Foreach](./process-collection-in-parallel-with-foreach.mdx) - Parallel iteration\n- [Understand Fibers as Lightweight Threads](./understand-fibers-as-lightweight-threads.mdx) - Fiber concurrency\n- [Concurrency Pattern 1: Coordinate with Deferred](./concurrency-pattern-coordinate-with-deferred.mdx) - Async signaling" - }, - { - "id": "concurrency-pattern-coordinate-with-latch", - "title": "Concurrency Pattern 3: Coordinate Multiple Fibers with Latch", - "description": "Use Latch to coordinate multiple fibers awaiting a common completion signal, enabling fan-out/fan-in and barrier synchronization patterns.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates a fan-out/fan-in pattern: spawn 5 worker fibers that process tasks in parallel, and coordinate to know when all are complete.\n\n```typescript\nimport { Effect, Latch, Fiber, Ref } from \"effect\";\n\ninterface WorkResult {\n readonly workerId: number;\n readonly taskId: number;\n readonly result: string;\n readonly duration: number;\n}\n\n// Simulate a long-running task\nconst processTask = (\n workerId: number,\n taskId: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n const duration = 100 + Math.random() * 400; // 100-500ms\n\n yield* Effect.log(\n `[Worker ${workerId}] Starting task ${taskId} (duration: ${Math.round(duration)}ms)`\n );\n\n yield* Effect.sleep(`${Math.round(duration)} millis`);\n\n const elapsed = Date.now() - startTime;\n\n yield* Effect.log(\n `[Worker ${workerId}] ✓ Completed task ${taskId} in ${elapsed}ms`\n );\n\n return {\n workerId,\n taskId,\n result: `Result from worker ${workerId} on task ${taskId}`,\n duration: elapsed,\n };\n });\n\n// Fan-out/Fan-in with Latch\nconst fanOutFanIn = Effect.gen(function* () {\n const numWorkers = 5;\n const tasksPerWorker = 3;\n\n // Create latch: will countdown from (numWorkers) when all workers complete\n const workersCompleteLatch = yield* Latch.make(numWorkers);\n\n // Track results from all workers\n const results = yield* Ref.make([]);\n\n // Worker fiber that processes tasks sequentially\n const createWorker = (workerId: number) =>\n Effect.gen(function* () {\n try {\n yield* Effect.log(`[Worker ${workerId}] ▶ Starting`);\n\n // Process multiple tasks\n for (let i = 1; i <= tasksPerWorker; i++) {\n const result = yield* processTask(workerId, i);\n yield* Ref.update(results, (rs) => [...rs, result]);\n }\n\n yield* Effect.log(`[Worker ${workerId}] ✓ All tasks completed`);\n } finally {\n // Signal completion to latch\n yield* Latch.countDown(workersCompleteLatch);\n yield* Effect.log(`[Worker ${workerId}] Signaled latch`);\n }\n });\n\n // Spawn all workers as background fibers\n console.log(`\\n[COORDINATOR] Spawning ${numWorkers} workers...\\n`);\n\n const workerFibers = yield* Effect.all(\n Array.from({ length: numWorkers }, (_, i) =>\n createWorker(i + 1).pipe(Effect.fork)\n )\n );\n\n // Wait for all workers to complete\n console.log(`\\n[COORDINATOR] Waiting for all workers to finish...\\n`);\n\n yield* Latch.await(workersCompleteLatch);\n\n console.log(`\\n[COORDINATOR] All workers completed!\\n`);\n\n // Join all fibers to ensure cleanup\n yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber)));\n\n // Aggregate results\n const allResults = yield* Ref.get(results);\n\n console.log(`[SUMMARY]`);\n console.log(` Total workers: ${numWorkers}`);\n console.log(` Tasks per worker: ${tasksPerWorker}`);\n console.log(` Total tasks: ${allResults.length}`);\n console.log(\n ` Avg task duration: ${Math.round(\n allResults.reduce((sum, r) => sum + r.duration, 0) / allResults.length\n )}ms`\n );\n});\n\nEffect.runPromise(fanOutFanIn);\n```\n\nThis pattern:\n\n1. **Creates Latch** with count = number of workers\n2. **Spawns worker fibers** as background tasks\n3. **Each worker processes tasks** independently\n4. **Signals Latch** when work completes (countDown)\n5. **Coordinator awaits** until all workers signal\n6. **Aggregates results** from all workers\n\n---", - "antiPattern": "", - "explanation": "Multi-fiber coordination requires synchronization:\n\n- **Parallel initialization**: Wait for all services to start before proceeding\n- **Fan-out/fan-in**: Spawn multiple workers, collect results when all done\n- **Barrier synchronization**: All fibers wait at a checkpoint before proceeding\n- **Graceful shutdown**: Wait for all active fibers to complete\n- **Aggregation patterns**: Process streams in parallel, combine when ready\n\nUnlike `Deferred` (one producer signals once), `Latch`:\n\n- Supports multiple signalers (each `countDown()`)\n- Used with known count of participants (countdown from N to 0)\n- Enables barrier patterns (all wait for all)\n- Fair queuing of waiting fibers\n\n---", - "content": "## Guideline\n\nWhen you need multiple fibers to coordinate and wait for a shared completion condition, use `Latch`. A Latch is a countdown synchronization object: you initialize it with N, each fiber calls `countDown()`, and all waiting fibers are released when the count reaches zero. This enables fan-out/fan-in patterns and barrier synchronization.\n\n---\n\n## Rationale\n\nMulti-fiber coordination requires synchronization:\n\n- **Parallel initialization**: Wait for all services to start before proceeding\n- **Fan-out/fan-in**: Spawn multiple workers, collect results when all done\n- **Barrier synchronization**: All fibers wait at a checkpoint before proceeding\n- **Graceful shutdown**: Wait for all active fibers to complete\n- **Aggregation patterns**: Process streams in parallel, combine when ready\n\nUnlike `Deferred` (one producer signals once), `Latch`:\n\n- Supports multiple signalers (each `countDown()`)\n- Used with known count of participants (countdown from N to 0)\n- Enables barrier patterns (all wait for all)\n- Fair queuing of waiting fibers\n\n---\n\n## Good Example\n\nThis example demonstrates a fan-out/fan-in pattern: spawn 5 worker fibers that process tasks in parallel, and coordinate to know when all are complete.\n\n```typescript\nimport { Effect, Latch, Fiber, Ref } from \"effect\";\n\ninterface WorkResult {\n readonly workerId: number;\n readonly taskId: number;\n readonly result: string;\n readonly duration: number;\n}\n\n// Simulate a long-running task\nconst processTask = (\n workerId: number,\n taskId: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n const duration = 100 + Math.random() * 400; // 100-500ms\n\n yield* Effect.log(\n `[Worker ${workerId}] Starting task ${taskId} (duration: ${Math.round(duration)}ms)`\n );\n\n yield* Effect.sleep(`${Math.round(duration)} millis`);\n\n const elapsed = Date.now() - startTime;\n\n yield* Effect.log(\n `[Worker ${workerId}] ✓ Completed task ${taskId} in ${elapsed}ms`\n );\n\n return {\n workerId,\n taskId,\n result: `Result from worker ${workerId} on task ${taskId}`,\n duration: elapsed,\n };\n });\n\n// Fan-out/Fan-in with Latch\nconst fanOutFanIn = Effect.gen(function* () {\n const numWorkers = 5;\n const tasksPerWorker = 3;\n\n // Create latch: will countdown from (numWorkers) when all workers complete\n const workersCompleteLatch = yield* Latch.make(numWorkers);\n\n // Track results from all workers\n const results = yield* Ref.make([]);\n\n // Worker fiber that processes tasks sequentially\n const createWorker = (workerId: number) =>\n Effect.gen(function* () {\n try {\n yield* Effect.log(`[Worker ${workerId}] ▶ Starting`);\n\n // Process multiple tasks\n for (let i = 1; i <= tasksPerWorker; i++) {\n const result = yield* processTask(workerId, i);\n yield* Ref.update(results, (rs) => [...rs, result]);\n }\n\n yield* Effect.log(`[Worker ${workerId}] ✓ All tasks completed`);\n } finally {\n // Signal completion to latch\n yield* Latch.countDown(workersCompleteLatch);\n yield* Effect.log(`[Worker ${workerId}] Signaled latch`);\n }\n });\n\n // Spawn all workers as background fibers\n console.log(`\\n[COORDINATOR] Spawning ${numWorkers} workers...\\n`);\n\n const workerFibers = yield* Effect.all(\n Array.from({ length: numWorkers }, (_, i) =>\n createWorker(i + 1).pipe(Effect.fork)\n )\n );\n\n // Wait for all workers to complete\n console.log(`\\n[COORDINATOR] Waiting for all workers to finish...\\n`);\n\n yield* Latch.await(workersCompleteLatch);\n\n console.log(`\\n[COORDINATOR] All workers completed!\\n`);\n\n // Join all fibers to ensure cleanup\n yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber)));\n\n // Aggregate results\n const allResults = yield* Ref.get(results);\n\n console.log(`[SUMMARY]`);\n console.log(` Total workers: ${numWorkers}`);\n console.log(` Tasks per worker: ${tasksPerWorker}`);\n console.log(` Total tasks: ${allResults.length}`);\n console.log(\n ` Avg task duration: ${Math.round(\n allResults.reduce((sum, r) => sum + r.duration, 0) / allResults.length\n )}ms`\n );\n});\n\nEffect.runPromise(fanOutFanIn);\n```\n\nThis pattern:\n\n1. **Creates Latch** with count = number of workers\n2. **Spawns worker fibers** as background tasks\n3. **Each worker processes tasks** independently\n4. **Signals Latch** when work completes (countDown)\n5. **Coordinator awaits** until all workers signal\n6. **Aggregates results** from all workers\n\n---\n\n## Advanced: Barrier Synchronization\n\nAll fibers wait at a checkpoint before proceeding:\n\n```typescript\ninterface WorkerConfig {\n readonly workerId: number;\n readonly phaseDuration: number;\n}\n\nconst barrierSynchronization = (workers: WorkerConfig[]) =>\n Effect.gen(function* () {\n const phases = 3;\n const barrierLatches = yield* Effect.all(\n Array.from({ length: phases }, () => Latch.make(workers.length))\n );\n\n // Worker that processes phases with barrier synchronization\n const createBarrierWorker = (config: WorkerConfig, barriers: Latch[]) =>\n Effect.gen(function* () {\n for (const [phase, barrier] of barriers.entries()) {\n yield* Effect.log(\n `[Worker ${config.workerId}] Phase ${phase + 1}: Working...`\n );\n\n // Simulate work\n yield* Effect.sleep(`${config.phaseDuration} millis`);\n\n yield* Effect.log(\n `[Worker ${config.workerId}] Phase ${phase + 1}: Done, waiting at barrier`\n );\n\n // Signal completion for this phase\n yield* Latch.countDown(barrier);\n\n // Wait for all workers to reach barrier\n yield* Latch.await(barrier);\n\n yield* Effect.log(\n `[Worker ${config.workerId}] Phase ${phase + 1}: All workers ready, proceeding`\n );\n }\n });\n\n // Spawn all workers with barrier coordination\n const fibers = yield* Effect.all(\n workers.map((w) =>\n createBarrierWorker(w, barrierLatches).pipe(Effect.fork)\n )\n );\n\n // Wait for all to complete\n yield* Effect.all(fibers.map((f) => Fiber.join(f)));\n });\n\nconst barrierExample = barrierSynchronization([\n { workerId: 1, phaseDuration: 100 },\n { workerId: 2, phaseDuration: 150 },\n { workerId: 3, phaseDuration: 120 },\n]);\n```\n\n---\n\n## Advanced: Hierarchical Coordination (Tree Join)\n\nCoordinate multiple stages of workers:\n\n```typescript\ninterface TreeJoinConfig {\n readonly stageCount: number;\n readonly workersPerStage: number;\n}\n\nconst treeJoinCoordination = (config: TreeJoinConfig) =>\n Effect.gen(function* () {\n const stageLocks: Latch[] = [];\n\n // Create latch for each stage\n for (let stage = 0; stage < config.stageCount; stage++) {\n const latch = yield* Latch.make(config.workersPerStage);\n stageLocks.push(latch);\n }\n\n // Worker that participates in stage-based coordination\n const createStageWorker = (\n stageIndex: number,\n workerId: number\n ): Effect.Effect =>\n Effect.gen(function* () {\n // Only run if this stage has workers\n if (stageIndex < config.stageCount) {\n yield* Effect.log(\n `[Stage ${stageIndex}] Worker ${workerId} processing...`\n );\n\n // Simulate work\n yield* Effect.sleep(`${50 + Math.random() * 100} millis`);\n\n yield* Effect.log(\n `[Stage ${stageIndex}] Worker ${workerId} done, signaling`\n );\n\n // Signal completion\n yield* Latch.countDown(stageLocks[stageIndex]);\n\n // Wait for all workers in this stage\n yield* Latch.await(stageLocks[stageIndex]);\n\n yield* Effect.log(\n `[Stage ${stageIndex}] All workers ready, proceeding to next stage`\n );\n\n // Recursively proceed to next stage\n if (stageIndex + 1 < config.stageCount) {\n yield* createStageWorker(stageIndex + 1, workerId);\n }\n }\n });\n\n // Start all workers at stage 0\n const fibers = yield* Effect.all(\n Array.from({ length: config.workersPerStage }, (_, i) =>\n createStageWorker(0, i + 1).pipe(Effect.fork)\n )\n );\n\n // Wait for completion\n yield* Effect.all(fibers.map((f) => Fiber.join(f)));\n });\n```\n\n---\n\n## Advanced: Error Propagation with Latch\n\nHandle failures in coordinated fibers:\n\n```typescript\ninterface CoordinatedTask {\n readonly taskId: number;\n readonly shouldFail: boolean;\n}\n\nconst coordinatedWithErrorHandling = (\n tasks: CoordinatedTask[]\n) =>\n Effect.gen(function* () {\n const completionLatch = yield* Latch.make(tasks.length);\n const errors = yield* Ref.make([]);\n const results = yield* Ref.make>(new Map());\n\n // Worker that can fail\n const executeTask = (task: CoordinatedTask) =>\n Effect.gen(function* () {\n try {\n yield* Effect.log(`[Task ${task.taskId}] Starting...`);\n\n if (task.shouldFail) {\n yield* Effect.sleep(\"50 millis\");\n throw new Error(`Task ${task.taskId} intentionally failed`);\n }\n\n yield* Effect.sleep(\"100 millis\");\n yield* Ref.update(results, (m) =>\n m.set(task.taskId, `Success from task ${task.taskId}`)\n );\n\n yield* Effect.log(`[Task ${task.taskId}] ✓ Completed`);\n } catch (error) {\n yield* Ref.update(errors, (errs) => [...errs, error as Error]);\n yield* Effect.log(`[Task ${task.taskId}] ✗ Failed: ${error}`);\n } finally {\n // Always signal completion\n yield* Latch.countDown(completionLatch);\n }\n });\n\n // Execute all tasks\n const fibers = yield* Effect.all(\n tasks.map((t) => executeTask(t).pipe(Effect.fork))\n );\n\n // Wait for all to complete\n yield* Latch.await(completionLatch);\n yield* Effect.all(fibers.map((f) => Fiber.join(f)));\n\n // Check for errors\n const taskErrors = yield* Ref.get(errors);\n if (taskErrors.length > 0) {\n yield* Effect.log(\n `\\n⚠ ${taskErrors.length} task(s) failed during coordination:`\n );\n taskErrors.forEach((err, idx) =>\n console.log(` ${idx + 1}. ${err.message}`)\n );\n }\n\n const allResults = yield* Ref.get(results);\n yield* Effect.log(\n `\\n✓ ${allResults.size} task(s) succeeded during coordination`\n );\n });\n```\n\n---\n\n## Advanced: Timeout-Based Coordination\n\nProceed even if some fibers don't complete:\n\n```typescript\nconst coordinatedWithTimeout = (\n taskCount: number,\n timeoutMs: number\n) =>\n Effect.gen(function* () {\n const completionLatch = yield* Latch.make(taskCount);\n const completed = yield* Ref.make(0);\n\n // Task that might hang\n const unreliableTask = (taskId: number) =>\n Effect.gen(function* () {\n const delay = Math.random() > 0.5 ? 1000 : 200; // 50% chance of timeout\n\n yield* Effect.log(\n `[Task ${taskId}] Starting with ${delay}ms delay`\n );\n\n yield* Effect.sleep(`${delay} millis`).pipe(\n Effect.catchAll(() => Effect.void)\n );\n\n yield* Ref.update(completed, (c) => c + 1);\n yield* Latch.countDown(completionLatch);\n\n yield* Effect.log(`[Task ${taskId}] Completed`);\n });\n\n // Spawn tasks\n const fibers = yield* Effect.all(\n Array.from({ length: taskCount }, (_, i) =>\n unreliableTask(i + 1).pipe(Effect.fork)\n )\n );\n\n // Wait with timeout\n const waitResult = yield* Latch.await(completionLatch).pipe(\n Effect.timeout(`${timeoutMs} millis`),\n Effect.either\n );\n\n if (waitResult._tag === \"Left\") {\n yield* Effect.log(\n `⚠ Coordination timeout after ${timeoutMs}ms`\n );\n } else {\n yield* Effect.log(`✓ All tasks completed within timeout`);\n }\n\n // Join fibers\n yield* Effect.all(fibers.map((f) => Fiber.join(f)));\n\n const completedCount = yield* Ref.get(completed);\n yield* Effect.log(\n `[RESULT] ${completedCount}/${taskCount} tasks completed`\n );\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Latch when:**\n\n- Coordinating N fibers with a completion condition\n- Implementing fan-out/fan-in patterns\n- Creating barrier synchronization (all wait for all)\n- Waiting for parallel initialization to complete\n- Coordinating multi-stage pipelines\n- Need to know when all workers are done\n\n⚠️ **Trade-offs:**\n\n- Latch is one-time use (can't reset count)\n- All N fibers must reach latch for waiters to proceed\n- If any fiber hangs, coordinated fibers wait indefinitely\n- Requires explicit countDown() calls (can be forgotten)\n\n---\n\n## When to Choose Between Latch and Deferred\n\n| Scenario | Latch | Deferred |\n| --- | --- | --- |\n| One producer signals many consumers | Deferred | ✓ Simpler |\n| N producers signal N consumers | ✓ Latch | - |\n| One-time coordination | Both | Both |\n| Reusable coordination | - | Use multiple Deferreds |\n| Known participant count | ✓ Latch | - |\n| Unknown participant count | - | ✓ Deferred |\n\n---\n\n## See Also\n\n- [Concurrency Pattern 1: Coordinate with Deferred](./concurrency-pattern-coordinate-with-deferred.mdx) - One-time async signaling\n- [Concurrency Pattern 2: Rate Limit with Semaphore](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Concurrent resource limiting\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background fiber execution\n- [Process Collection in Parallel with Foreach](./process-collection-in-parallel-with-foreach.mdx) - Parallel iteration" - }, - { - "id": "concurrency-pattern-queue-work-distribution", - "title": "Concurrency Pattern 4: Distribute Work with Queue", - "description": "Use Queue to distribute work between producers and consumers with built-in backpressure, enabling flexible pipeline coordination.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates a producer-consumer pipeline with a bounded queue for buffering work items.\n\n```typescript\nimport { Effect, Queue, Fiber, Ref } from \"effect\";\n\ninterface WorkItem {\n readonly id: number;\n readonly data: string;\n readonly timestamp: number;\n}\n\ninterface WorkResult {\n readonly itemId: number;\n readonly processed: string;\n readonly duration: number;\n}\n\n// Producer: generates work items\nconst producer = (\n queue: Queue.Enqueue,\n count: number\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[PRODUCER] Starting, generating ${count} items`);\n\n for (let i = 1; i <= count; i++) {\n const item: WorkItem = {\n id: i,\n data: `Item ${i}`,\n timestamp: Date.now(),\n };\n\n const start = Date.now();\n\n // Enqueue - will block if queue is full (backpressure)\n yield* Queue.offer(queue, item);\n\n const delay = Date.now() - start;\n\n if (delay > 0) {\n yield* Effect.log(\n `[PRODUCER] Item ${i} enqueued (waited ${delay}ms due to backpressure)`\n );\n } else {\n yield* Effect.log(`[PRODUCER] Item ${i} enqueued`);\n }\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\");\n }\n\n yield* Effect.log(`[PRODUCER] ✓ All items enqueued`);\n });\n\n// Consumer: processes work items\nconst consumer = (\n queue: Queue.Dequeue,\n consumerId: number,\n results: Ref.Ref\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONSUMER ${consumerId}] Starting`);\n\n while (true) {\n // Dequeue - will block if queue is empty\n const item = yield* Queue.take(queue).pipe(Effect.either);\n\n if (item._tag === \"Left\") {\n yield* Effect.log(`[CONSUMER ${consumerId}] Queue closed, stopping`);\n return;\n }\n\n const workItem = item.right;\n const startTime = Date.now();\n\n yield* Effect.log(\n `[CONSUMER ${consumerId}] Processing ${workItem.data}`\n );\n\n // Simulate processing\n yield* Effect.sleep(\"150 millis\");\n\n const duration = Date.now() - startTime;\n const result: WorkResult = {\n itemId: workItem.id,\n processed: `${workItem.data} [processed by consumer ${consumerId}]`,\n duration,\n };\n\n yield* Ref.update(results, (rs) => [...rs, result]);\n\n yield* Effect.log(\n `[CONSUMER ${consumerId}] ✓ Completed ${workItem.data} in ${duration}ms`\n );\n }\n });\n\n// Main: coordinate producer and consumers\nconst program = Effect.gen(function* () {\n // Create bounded queue with capacity 3\n const queue = yield* Queue.bounded(3);\n const results = yield* Ref.make([]);\n\n console.log(`\\n[MAIN] Starting producer-consumer pipeline with queue size 3\\n`);\n\n // Spawn producer\n const producerFiber = yield* producer(queue, 10).pipe(Effect.fork);\n\n // Spawn 2 consumers\n const consumer1 = yield* consumer(queue, 1, results).pipe(Effect.fork);\n const consumer2 = yield* consumer(queue, 2, results).pipe(Effect.fork);\n\n // Wait for producer to finish\n yield* Fiber.join(producerFiber);\n\n // Give consumers time to finish\n yield* Effect.sleep(\"3 seconds\");\n\n // Close queue and wait for consumers\n yield* Queue.shutdown(queue);\n yield* Fiber.join(consumer1);\n yield* Fiber.join(consumer2);\n\n // Summary\n const allResults = yield* Ref.get(results);\n const totalDuration = allResults.reduce((sum, r) => sum + r.duration, 0);\n\n console.log(`\\n[SUMMARY]`);\n console.log(` Items processed: ${allResults.length}`);\n console.log(\n ` Avg processing time: ${Math.round(totalDuration / allResults.length)}ms`\n );\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates bounded queue** with capacity (backpressure point)\n2. **Producer enqueues** items (blocks if full)\n3. **Consumers dequeue** and process (each at own pace)\n4. **Queue coordinates** flow automatically\n\n---", - "antiPattern": "", - "explanation": "Direct producer-consumer coordination creates problems:\n\n- **Blocking**: Producer waits for consumer to finish\n- **Tight coupling**: Producer depends on consumer speed\n- **Memory pressure**: Fast producer floods memory with results\n- **No backpressure**: Downstream overload propagates upstream\n\nQueue solves these:\n\n- **Asynchronous**: Producer enqueues and continues\n- **Decoupled**: Producer/consumer independent\n- **Backpressure**: Producer waits when queue full (natural flow control)\n- **Throughput**: Consumer processes at own pace\n\nReal-world example: API request handler + database writer\n- **Direct**: Handler waits for DB write (blocking, slow requests)\n- **Queue**: Handler enqueues write and returns immediately (responsive)\n\n---", - "content": "## Guideline\n\nWhen multiple fibers need to coordinate work asynchronously, use `Queue`:\n\n- **Producers** add items (enqueue)\n- **Consumers** remove and process items (dequeue)\n- **Backpressure** built-in: producers wait if queue is full\n- **Decoupling**: Producers don't block on consumer speed\n\nQueue variants: `bounded` (size limit), `unbounded` (unlimited), `dropping` (discards on overflow).\n\n---\n\n## Rationale\n\nDirect producer-consumer coordination creates problems:\n\n- **Blocking**: Producer waits for consumer to finish\n- **Tight coupling**: Producer depends on consumer speed\n- **Memory pressure**: Fast producer floods memory with results\n- **No backpressure**: Downstream overload propagates upstream\n\nQueue solves these:\n\n- **Asynchronous**: Producer enqueues and continues\n- **Decoupled**: Producer/consumer independent\n- **Backpressure**: Producer waits when queue full (natural flow control)\n- **Throughput**: Consumer processes at own pace\n\nReal-world example: API request handler + database writer\n- **Direct**: Handler waits for DB write (blocking, slow requests)\n- **Queue**: Handler enqueues write and returns immediately (responsive)\n\n---\n\n## Good Example\n\nThis example demonstrates a producer-consumer pipeline with a bounded queue for buffering work items.\n\n```typescript\nimport { Effect, Queue, Fiber, Ref } from \"effect\";\n\ninterface WorkItem {\n readonly id: number;\n readonly data: string;\n readonly timestamp: number;\n}\n\ninterface WorkResult {\n readonly itemId: number;\n readonly processed: string;\n readonly duration: number;\n}\n\n// Producer: generates work items\nconst producer = (\n queue: Queue.Enqueue,\n count: number\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[PRODUCER] Starting, generating ${count} items`);\n\n for (let i = 1; i <= count; i++) {\n const item: WorkItem = {\n id: i,\n data: `Item ${i}`,\n timestamp: Date.now(),\n };\n\n const start = Date.now();\n\n // Enqueue - will block if queue is full (backpressure)\n yield* Queue.offer(queue, item);\n\n const delay = Date.now() - start;\n\n if (delay > 0) {\n yield* Effect.log(\n `[PRODUCER] Item ${i} enqueued (waited ${delay}ms due to backpressure)`\n );\n } else {\n yield* Effect.log(`[PRODUCER] Item ${i} enqueued`);\n }\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\");\n }\n\n yield* Effect.log(`[PRODUCER] ✓ All items enqueued`);\n });\n\n// Consumer: processes work items\nconst consumer = (\n queue: Queue.Dequeue,\n consumerId: number,\n results: Ref.Ref\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONSUMER ${consumerId}] Starting`);\n\n while (true) {\n // Dequeue - will block if queue is empty\n const item = yield* Queue.take(queue).pipe(Effect.either);\n\n if (item._tag === \"Left\") {\n yield* Effect.log(`[CONSUMER ${consumerId}] Queue closed, stopping`);\n return;\n }\n\n const workItem = item.right;\n const startTime = Date.now();\n\n yield* Effect.log(\n `[CONSUMER ${consumerId}] Processing ${workItem.data}`\n );\n\n // Simulate processing\n yield* Effect.sleep(\"150 millis\");\n\n const duration = Date.now() - startTime;\n const result: WorkResult = {\n itemId: workItem.id,\n processed: `${workItem.data} [processed by consumer ${consumerId}]`,\n duration,\n };\n\n yield* Ref.update(results, (rs) => [...rs, result]);\n\n yield* Effect.log(\n `[CONSUMER ${consumerId}] ✓ Completed ${workItem.data} in ${duration}ms`\n );\n }\n });\n\n// Main: coordinate producer and consumers\nconst program = Effect.gen(function* () {\n // Create bounded queue with capacity 3\n const queue = yield* Queue.bounded(3);\n const results = yield* Ref.make([]);\n\n console.log(`\\n[MAIN] Starting producer-consumer pipeline with queue size 3\\n`);\n\n // Spawn producer\n const producerFiber = yield* producer(queue, 10).pipe(Effect.fork);\n\n // Spawn 2 consumers\n const consumer1 = yield* consumer(queue, 1, results).pipe(Effect.fork);\n const consumer2 = yield* consumer(queue, 2, results).pipe(Effect.fork);\n\n // Wait for producer to finish\n yield* Fiber.join(producerFiber);\n\n // Give consumers time to finish\n yield* Effect.sleep(\"3 seconds\");\n\n // Close queue and wait for consumers\n yield* Queue.shutdown(queue);\n yield* Fiber.join(consumer1);\n yield* Fiber.join(consumer2);\n\n // Summary\n const allResults = yield* Ref.get(results);\n const totalDuration = allResults.reduce((sum, r) => sum + r.duration, 0);\n\n console.log(`\\n[SUMMARY]`);\n console.log(` Items processed: ${allResults.length}`);\n console.log(\n ` Avg processing time: ${Math.round(totalDuration / allResults.length)}ms`\n );\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates bounded queue** with capacity (backpressure point)\n2. **Producer enqueues** items (blocks if full)\n3. **Consumers dequeue** and process (each at own pace)\n4. **Queue coordinates** flow automatically\n\n---\n\n## Advanced: Dynamic Consumer Pool\n\nScale consumer count based on queue depth:\n\n```typescript\nconst adaptiveConsumerPool = (\n queue: Queue.Dequeue,\n maxConsumers: number\n) =>\n Effect.gen(function* () {\n const activeConsumers = yield* Ref.make(1);\n const metrics = yield* Ref.make({\n itemsProcessed: 0,\n queueDepth: 0,\n avgProcessTime: 0,\n });\n\n // Monitor queue and scale consumers\n const scaler = Effect.gen(function* () {\n while (true) {\n yield* Effect.sleep(\"500 millis\");\n\n const depth = yield* Queue.size(queue);\n const consumers = yield* Ref.get(activeConsumers);\n\n // Scale up if queue building\n if (depth > consumers * 2 && consumers < maxConsumers) {\n yield* Ref.update(activeConsumers, (c) => c + 1);\n yield* Effect.log(\n `[SCALER] Increased to ${consumers + 1} consumers (queue depth: ${depth})`\n );\n }\n\n // Scale down if queue draining\n if (depth < consumers / 2 && consumers > 1) {\n yield* Ref.update(activeConsumers, (c) => c - 1);\n yield* Effect.log(\n `[SCALER] Decreased to ${consumers - 1} consumers (queue depth: ${depth})`\n );\n }\n }\n });\n\n return scaler;\n });\n```\n\n---\n\n## Advanced: Priority Queue with Multiple Queues\n\nSeparate queues for different priority levels:\n\n```typescript\ninterface PriorityWorkItem extends WorkItem {\n readonly priority: number; // Higher = more important\n}\n\nconst createPriorityQueueSystem = () =>\n Effect.gen(function* () {\n // High priority queue (size 10), normal queue (size 50)\n const highPriorityQueue = yield* Queue.bounded(10);\n const normalQueue = yield* Queue.bounded(50);\n\n const enqueueWithPriority = (item: PriorityWorkItem) =>\n Effect.gen(function* () {\n if (item.priority > 5) {\n yield* Queue.offer(highPriorityQueue, item);\n yield* Effect.log(\n `[PRIORITY] Item ${item.id} enqueued to HIGH priority queue`\n );\n } else {\n yield* Queue.offer(normalQueue, item);\n yield* Effect.log(\n `[PRIORITY] Item ${item.id} enqueued to NORMAL queue`\n );\n }\n });\n\n // Consumer prioritizes high-priority queue\n const priorityConsumer = (consumerId: number) =>\n Effect.gen(function* () {\n while (true) {\n // Try high priority first\n const highItem = yield* Queue.poll(highPriorityQueue);\n\n if (highItem._tag === \"Some\") {\n yield* Effect.log(\n `[CONSUMER ${consumerId}] Processing HIGH priority item ${highItem.value.id}`\n );\n yield* Effect.sleep(\"100 millis\");\n continue;\n }\n\n // Fall back to normal priority\n const normalItem = yield* Queue.poll(normalQueue);\n\n if (normalItem._tag === \"Some\") {\n yield* Effect.log(\n `[CONSUMER ${consumerId}] Processing NORMAL priority item ${normalItem.value.id}`\n );\n yield* Effect.sleep(\"100 millis\");\n continue;\n }\n\n // No items available, wait a bit\n yield* Effect.sleep(\"100 millis\");\n }\n });\n\n return {\n enqueueWithPriority,\n priorityConsumer,\n highPriorityQueue,\n normalQueue,\n };\n });\n```\n\n---\n\n## Advanced: Queue with Batch Processing\n\nDequeue multiple items and process as batch:\n\n```typescript\nconst batchConsumer = (\n queue: Queue.Dequeue,\n batchSize: number,\n processBatch: (items: WorkItem[]) => Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n while (true) {\n const batch: WorkItem[] = [];\n\n // Collect up to batchSize items (with timeout)\n for (let i = 0; i < batchSize; i++) {\n const item = yield* Queue.take(queue).pipe(\n Effect.timeout(\"100 millis\"),\n Effect.either\n );\n\n if (item._tag === \"Left\") {\n // Timeout - process whatever we have\n break;\n }\n\n batch.push(item.right);\n }\n\n if (batch.length > 0) {\n yield* Effect.log(\n `[BATCH] Processing ${batch.length} items`\n );\n\n yield* processBatch(batch);\n\n yield* Effect.log(\n `[BATCH] ✓ Completed batch of ${batch.length}`\n );\n } else {\n // No items, wait before retrying\n yield* Effect.sleep(\"100 millis\");\n }\n }\n });\n```\n\n---\n\n## Advanced: Async Pipeline with Multiple Stages\n\nChain multiple queue-based processing stages:\n\n```typescript\ninterface PipelineStage {\n readonly name: string;\n readonly inputQueue: Queue.Dequeue;\n readonly outputQueue: Queue.Enqueue;\n readonly process: (item: I) => Effect.Effect;\n}\n\nconst createPipeline = (\n inputQueue: Queue.Dequeue,\n stages: Array<(item: I) => Effect.Effect>,\n outputQueue: Queue.Enqueue\n): Effect.Effect =>\n Effect.gen(function* () {\n while (true) {\n const item = yield* Queue.take(inputQueue);\n let current: any = item;\n\n // Process through all stages\n for (const stage of stages) {\n current = yield* stage(current);\n }\n\n yield* Queue.offer(outputQueue, current);\n }\n });\n\n// Example: 3-stage pipeline\nconst pipelineExample = Effect.gen(function* () {\n const inputQueue = yield* Queue.bounded(10);\n const outputQueue = yield* Queue.bounded(10);\n\n const stages = [\n (item: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STAGE 1] Validating: ${item}`);\n return item.toUpperCase();\n }),\n\n (item: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STAGE 2] Enriching: ${item}`);\n return `${item}-ENRICHED`;\n }),\n\n (item: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STAGE 3] Formatting: ${item}`);\n return `[${item}]`;\n }),\n ];\n\n yield* createPipeline(inputQueue, stages, outputQueue);\n});\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Queue when:**\n\n- Decoupling producers from consumers\n- Implementing pipeline stages\n- Handling backpressure naturally\n- Work distribution across fibers\n- Async work buffering\n- Load balancing across workers\n\n⚠️ **Trade-offs:**\n\n- Queue copy overhead for large items\n- Memory usage grows with queue size\n- Bounded queues block producers\n- Unbounded queues can exhaust memory\n\n---\n\n## Queue Type Guide\n\n| Type | Behavior | Use Case |\n| --- | --- | --- |\n| `bounded(n)` | Backpressure when full | Producer/consumer control |\n| `unbounded()` | Grows unbounded | Unbounded work streams |\n| `dropping()` | Discards on overflow | High-frequency events (sampling) |\n| `sliding()` | Removes oldest on overflow | Sliding window buffering |\n\n---\n\n## See Also\n\n- [Decouple Fibers with Queue/PubSub](./decouple-fibers-with-queue-pubsub.mdx) - Queue basics\n- [Concurrency Pattern 3: Coordinate with Latch](./concurrency-pattern-coordinate-with-latch.mdx) - Multi-fiber sync\n- [Concurrency Pattern 2: Rate Limit with Semaphore](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Resource limiting\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background fibers" - }, - { - "id": "concurrency-pattern-pubsub-event-broadcast", - "title": "Concurrency Pattern 5: Broadcast Events with PubSub", - "description": "Use PubSub to broadcast events to multiple subscribers, enabling event-driven architectures where publishers and subscribers are loosely coupled.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates a multi-subscriber event broadcast system with independent handlers.\n\n```typescript\nimport { Effect, PubSub, Fiber, Ref } from \"effect\";\n\ninterface StateChangeEvent {\n readonly id: string;\n readonly oldValue: string;\n readonly newValue: string;\n readonly timestamp: number;\n}\n\ninterface Subscriber {\n readonly name: string;\n readonly events: StateChangeEvent[];\n}\n\n// Create subscribers that react to events\nconst createSubscriber = (\n name: string,\n pubsub: PubSub.PubSub,\n events: Ref.Ref\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[${name}] ✓ Subscribed`);\n\n // Get subscriber handle\n const subscription = yield* PubSub.subscribe(pubsub);\n\n // Listen for events indefinitely\n while (true) {\n const event = yield* subscription.take();\n\n yield* Effect.log(\n `[${name}] Received event: ${event.oldValue} → ${event.newValue}`\n );\n\n // Simulate processing\n yield* Effect.sleep(\"50 millis\");\n\n // Store event (example action)\n yield* Ref.update(events, (es) => [...es, event]);\n\n yield* Effect.log(`[${name}] ✓ Processed event`);\n }\n });\n\n// Publisher that broadcasts events\nconst publisher = (\n pubsub: PubSub.PubSub,\n eventCount: number\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[PUBLISHER] Starting, publishing ${eventCount} events`);\n\n for (let i = 1; i <= eventCount; i++) {\n const event: StateChangeEvent = {\n id: `event-${i}`,\n oldValue: `state-${i - 1}`,\n newValue: `state-${i}`,\n timestamp: Date.now(),\n };\n\n // Publish to all subscribers\n const size = yield* PubSub.publish(pubsub, event);\n\n yield* Effect.log(\n `[PUBLISHER] Published event to ${size} subscribers`\n );\n\n // Simulate delay between events\n yield* Effect.sleep(\"200 millis\");\n }\n\n yield* Effect.log(`[PUBLISHER] ✓ All events published`);\n });\n\n// Main: coordinate publisher and multiple subscribers\nconst program = Effect.gen(function* () {\n // Create PubSub with bounded capacity\n const pubsub = yield* PubSub.bounded(5);\n\n // Create storage for each subscriber's events\n const subscriber1Events = yield* Ref.make([]);\n const subscriber2Events = yield* Ref.make([]);\n const subscriber3Events = yield* Ref.make([]);\n\n console.log(`\\n[MAIN] Starting PubSub event broadcast system\\n`);\n\n // Subscribe 3 independent subscribers\n const sub1Fiber = yield* createSubscriber(\n \"SUBSCRIBER-1\",\n pubsub,\n subscriber1Events\n ).pipe(Effect.fork);\n\n const sub2Fiber = yield* createSubscriber(\n \"SUBSCRIBER-2\",\n pubsub,\n subscriber2Events\n ).pipe(Effect.fork);\n\n const sub3Fiber = yield* createSubscriber(\n \"SUBSCRIBER-3\",\n pubsub,\n subscriber3Events\n ).pipe(Effect.fork);\n\n // Wait for subscriptions to establish\n yield* Effect.sleep(\"100 millis\");\n\n // Start publisher\n const publisherFiber = yield* publisher(pubsub, 5).pipe(Effect.fork);\n\n // Wait for publisher to finish\n yield* Fiber.join(publisherFiber);\n\n // Wait a bit for subscribers to process last events\n yield* Effect.sleep(\"1 second\");\n\n // Shut down\n yield* PubSub.shutdown(pubsub);\n yield* Fiber.join(sub1Fiber).pipe(Effect.catchAll(() => Effect.void));\n yield* Fiber.join(sub2Fiber).pipe(Effect.catchAll(() => Effect.void));\n yield* Fiber.join(sub3Fiber).pipe(Effect.catchAll(() => Effect.void));\n\n // Print summary\n const events1 = yield* Ref.get(subscriber1Events);\n const events2 = yield* Ref.get(subscriber2Events);\n const events3 = yield* Ref.get(subscriber3Events);\n\n console.log(`\\n[SUMMARY]`);\n console.log(` Subscriber 1 received: ${events1.length} events`);\n console.log(` Subscriber 2 received: ${events2.length} events`);\n console.log(` Subscriber 3 received: ${events3.length} events`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates PubSub** for event distribution\n2. **Multiple subscribers** listen independently\n3. **Publisher broadcasts** events to all\n4. **Each subscriber** processes at own pace\n\n---", - "antiPattern": "", - "explanation": "Event distribution without PubSub creates coupling:\n\n- **Direct references**: Publisher calls subscribers directly (tight coupling)\n- **Ordering issues**: Publisher blocks on slowest subscriber\n- **Scalability**: Adding subscribers slows down publisher\n- **Testing**: Hard to mock multiple subscribers\n\nPubSub enables:\n\n- **Loose coupling**: Publishers emit, subscribers listen independently\n- **Parallel delivery**: All subscribers notified simultaneously\n- **Scalability**: Add subscribers without affecting publisher\n- **Testing**: Mock single PubSub rather than all subscribers\n\nReal-world example: System state changes\n- **Direct**: StateManager calls UserNotifier, AuditLogger, MetricsCollector (tight coupling)\n- **PubSub**: StateManager publishes `StateChanged` event; subscribers listen independently\n\n---", - "content": "## Guideline\n\nWhen multiple fibers need to react to the same events, use `PubSub`:\n\n- **Publisher** sends events once\n- **Subscribers** each receive a copy\n- **Decoupled**: Publisher doesn't know about subscribers\n- **Fan-out**: One event → multiple independent handlers\n\nPubSub variants: `bounded` (backpressure), `unbounded`, `sliding`.\n\n---\n\n## Rationale\n\nEvent distribution without PubSub creates coupling:\n\n- **Direct references**: Publisher calls subscribers directly (tight coupling)\n- **Ordering issues**: Publisher blocks on slowest subscriber\n- **Scalability**: Adding subscribers slows down publisher\n- **Testing**: Hard to mock multiple subscribers\n\nPubSub enables:\n\n- **Loose coupling**: Publishers emit, subscribers listen independently\n- **Parallel delivery**: All subscribers notified simultaneously\n- **Scalability**: Add subscribers without affecting publisher\n- **Testing**: Mock single PubSub rather than all subscribers\n\nReal-world example: System state changes\n- **Direct**: StateManager calls UserNotifier, AuditLogger, MetricsCollector (tight coupling)\n- **PubSub**: StateManager publishes `StateChanged` event; subscribers listen independently\n\n---\n\n## Good Example\n\nThis example demonstrates a multi-subscriber event broadcast system with independent handlers.\n\n```typescript\nimport { Effect, PubSub, Fiber, Ref } from \"effect\";\n\ninterface StateChangeEvent {\n readonly id: string;\n readonly oldValue: string;\n readonly newValue: string;\n readonly timestamp: number;\n}\n\ninterface Subscriber {\n readonly name: string;\n readonly events: StateChangeEvent[];\n}\n\n// Create subscribers that react to events\nconst createSubscriber = (\n name: string,\n pubsub: PubSub.PubSub,\n events: Ref.Ref\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[${name}] ✓ Subscribed`);\n\n // Get subscriber handle\n const subscription = yield* PubSub.subscribe(pubsub);\n\n // Listen for events indefinitely\n while (true) {\n const event = yield* subscription.take();\n\n yield* Effect.log(\n `[${name}] Received event: ${event.oldValue} → ${event.newValue}`\n );\n\n // Simulate processing\n yield* Effect.sleep(\"50 millis\");\n\n // Store event (example action)\n yield* Ref.update(events, (es) => [...es, event]);\n\n yield* Effect.log(`[${name}] ✓ Processed event`);\n }\n });\n\n// Publisher that broadcasts events\nconst publisher = (\n pubsub: PubSub.PubSub,\n eventCount: number\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[PUBLISHER] Starting, publishing ${eventCount} events`);\n\n for (let i = 1; i <= eventCount; i++) {\n const event: StateChangeEvent = {\n id: `event-${i}`,\n oldValue: `state-${i - 1}`,\n newValue: `state-${i}`,\n timestamp: Date.now(),\n };\n\n // Publish to all subscribers\n const size = yield* PubSub.publish(pubsub, event);\n\n yield* Effect.log(\n `[PUBLISHER] Published event to ${size} subscribers`\n );\n\n // Simulate delay between events\n yield* Effect.sleep(\"200 millis\");\n }\n\n yield* Effect.log(`[PUBLISHER] ✓ All events published`);\n });\n\n// Main: coordinate publisher and multiple subscribers\nconst program = Effect.gen(function* () {\n // Create PubSub with bounded capacity\n const pubsub = yield* PubSub.bounded(5);\n\n // Create storage for each subscriber's events\n const subscriber1Events = yield* Ref.make([]);\n const subscriber2Events = yield* Ref.make([]);\n const subscriber3Events = yield* Ref.make([]);\n\n console.log(`\\n[MAIN] Starting PubSub event broadcast system\\n`);\n\n // Subscribe 3 independent subscribers\n const sub1Fiber = yield* createSubscriber(\n \"SUBSCRIBER-1\",\n pubsub,\n subscriber1Events\n ).pipe(Effect.fork);\n\n const sub2Fiber = yield* createSubscriber(\n \"SUBSCRIBER-2\",\n pubsub,\n subscriber2Events\n ).pipe(Effect.fork);\n\n const sub3Fiber = yield* createSubscriber(\n \"SUBSCRIBER-3\",\n pubsub,\n subscriber3Events\n ).pipe(Effect.fork);\n\n // Wait for subscriptions to establish\n yield* Effect.sleep(\"100 millis\");\n\n // Start publisher\n const publisherFiber = yield* publisher(pubsub, 5).pipe(Effect.fork);\n\n // Wait for publisher to finish\n yield* Fiber.join(publisherFiber);\n\n // Wait a bit for subscribers to process last events\n yield* Effect.sleep(\"1 second\");\n\n // Shut down\n yield* PubSub.shutdown(pubsub);\n yield* Fiber.join(sub1Fiber).pipe(Effect.catchAll(() => Effect.void));\n yield* Fiber.join(sub2Fiber).pipe(Effect.catchAll(() => Effect.void));\n yield* Fiber.join(sub3Fiber).pipe(Effect.catchAll(() => Effect.void));\n\n // Print summary\n const events1 = yield* Ref.get(subscriber1Events);\n const events2 = yield* Ref.get(subscriber2Events);\n const events3 = yield* Ref.get(subscriber3Events);\n\n console.log(`\\n[SUMMARY]`);\n console.log(` Subscriber 1 received: ${events1.length} events`);\n console.log(` Subscriber 2 received: ${events2.length} events`);\n console.log(` Subscriber 3 received: ${events3.length} events`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates PubSub** for event distribution\n2. **Multiple subscribers** listen independently\n3. **Publisher broadcasts** events to all\n4. **Each subscriber** processes at own pace\n\n---\n\n## Advanced: Typed Event Channels\n\nRoute different event types through PubSub:\n\n```typescript\ninterface BaseEvent {\n readonly type: string;\n readonly timestamp: number;\n}\n\ninterface UserCreatedEvent extends BaseEvent {\n readonly type: \"UserCreated\";\n readonly userId: string;\n readonly email: string;\n}\n\ninterface UserDeletedEvent extends BaseEvent {\n readonly type: \"UserDeleted\";\n readonly userId: string;\n}\n\ntype UserEvent = UserCreatedEvent | UserDeletedEvent;\n\nconst createTypedSubscriber = (\n name: string,\n pubsub: PubSub.PubSub\n): Effect.Effect =>\n Effect.gen(function* () {\n const subscription = yield* PubSub.subscribe(pubsub);\n\n while (true) {\n const event = yield* subscription.take();\n\n // Pattern match on event type\n if (event.type === \"UserCreated\") {\n yield* Effect.log(\n `[${name}] New user created: ${event.email}`\n );\n } else if (event.type === \"UserDeleted\") {\n yield* Effect.log(\n `[${name}] User deleted: ${event.userId}`\n );\n }\n }\n });\n\nconst publishUserEvent = (\n pubsub: PubSub.PubSub,\n event: UserEvent\n) =>\n Effect.gen(function* () {\n const subscribers = yield* PubSub.publish(pubsub, event);\n yield* Effect.log(`Published to ${subscribers} subscribers`);\n });\n```\n\n---\n\n## Advanced: Filtered Subscriptions\n\nSubscribe to subset of events based on predicate:\n\n```typescript\ninterface FilteredSubscription {\n readonly name: string;\n readonly filter: (event: StateChangeEvent) => boolean;\n}\n\nconst createFilteredSubscriber = (\n config: FilteredSubscription,\n pubsub: PubSub.PubSub\n): Effect.Effect =>\n Effect.gen(function* () {\n const subscription = yield* PubSub.subscribe(pubsub);\n\n yield* Effect.log(\n `[${config.name}] Subscribed with filter`\n );\n\n while (true) {\n const event = yield* subscription.take();\n\n // Only process if matches filter\n if (config.filter(event)) {\n yield* Effect.log(\n `[${config.name}] Matched event: ${event.newValue}`\n );\n } else {\n yield* Effect.log(`[${config.name}] Filtered out event`);\n }\n }\n });\n\n// Example: Only listen to specific state changes\nconst criticalStateSubscriber = createFilteredSubscriber(\n {\n name: \"CRITICAL-MONITOR\",\n filter: (event) =>\n event.newValue.includes(\"ERROR\") ||\n event.newValue.includes(\"ALERT\"),\n },\n pubsub\n);\n```\n\n---\n\n## Advanced: Request-Reply with PubSub\n\nImplement request-response pattern on top of PubSub:\n\n```typescript\ninterface Request {\n readonly requestId: string;\n readonly type: string;\n readonly data: unknown;\n}\n\ninterface Response {\n readonly requestId: string;\n readonly status: \"success\" | \"error\";\n readonly result?: unknown;\n}\n\nconst requestReplyPattern = (\n requestPubSub: PubSub.PubSub,\n responsePubSub: PubSub.PubSub\n) =>\n Effect.gen(function* () {\n // Request handler\n const handleRequest = Effect.gen(function* () {\n const requestSub = yield* PubSub.subscribe(requestPubSub);\n\n while (true) {\n const request = yield* requestSub.take();\n\n yield* Effect.log(`[HANDLER] Processing request ${request.requestId}`);\n\n // Simulate processing\n yield* Effect.sleep(\"100 millis\");\n\n const response: Response = {\n requestId: request.requestId,\n status: \"success\",\n result: `Processed ${request.type}`,\n };\n\n yield* PubSub.publish(responsePubSub, response);\n }\n });\n\n // Client that sends request and listens for response\n const sendRequest = (request: Request) =>\n Effect.gen(function* () {\n // Publish request\n yield* PubSub.publish(requestPubSub, request);\n\n // Listen for matching response\n const responseSub = yield* PubSub.subscribe(\n responsePubSub\n );\n\n const response = yield* responseSub.take().pipe(\n Effect.repeatUntil(\n (r) => r.requestId === request.requestId\n )\n );\n\n return response;\n });\n\n return { handleRequest, sendRequest };\n });\n```\n\n---\n\n## Advanced: Event Aggregation\n\nCombine events from multiple publishers:\n\n```typescript\nconst eventAggregator = (\n sources: PubSub.PubSub[],\n aggregate: PubSub.PubSub\n): Effect.Effect =>\n Effect.gen(function* () {\n // Subscribe to all sources\n const subscriptions = yield* Effect.all(\n sources.map((source) => PubSub.subscribe(source))\n );\n\n // Forward all events to aggregate\n const forwarders = subscriptions.map((sub, idx) =>\n Effect.gen(function* () {\n yield* Effect.log(`[AGGREGATOR] Source ${idx + 1} connected`);\n\n while (true) {\n const event = yield* sub.take();\n yield* PubSub.publish(aggregate, event);\n }\n }).pipe(Effect.fork)\n );\n\n // Wait for all forwarders\n yield* Effect.all(forwarders.map((f) => Fiber.join(f)));\n });\n\n// Example: Aggregate events from 3 sources\nconst aggregatedEventBus = Effect.gen(function* () {\n const source1 = yield* PubSub.bounded(5);\n const source2 = yield* PubSub.bounded(5);\n const source3 = yield* PubSub.bounded(5);\n const eventBus = yield* PubSub.bounded(10);\n\n yield* eventAggregator(\n [source1, source2, source3],\n eventBus\n ).pipe(Effect.fork);\n\n return { source1, source2, source3, eventBus };\n});\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use PubSub when:**\n\n- Multiple subscribers need same event\n- Event-driven architecture\n- Loose coupling between components\n- One-to-many notifications\n- System state broadcasting\n- Event sourcing patterns\n\n⚠️ **Trade-offs:**\n\n- PubSub copies events to each subscriber\n- Subscribers can fall behind (queue buildup)\n- No acknowledgment mechanism\n- Broadcasting overhead for many subscribers\n\n---\n\n## PubSub vs Queue\n\n| Aspect | PubSub | Queue |\n| --- | --- | --- |\n| **Model** | Fan-out (1→many) | Pipeline (1→1) |\n| **Subscribers** | Multiple independent | Single consumer per item |\n| **Order** | All get same event | Items processed sequentially |\n| **Use Case** | Broadcasting | Work distribution |\n| **Coupling** | Loose | Loose |\n| **Scalability** | Grows with subscribers | Grows with items |\n\n---\n\n## See Also\n\n- [Decouple Fibers with Queue/PubSub](./decouple-fibers-with-queue-pubsub.mdx) - PubSub basics\n- [Concurrency Pattern 4: Queue Work Distribution](./concurrency-pattern-queue-work-distribution.mdx) - Work distribution\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background execution\n- [Concurrency Pattern 3: Coordinate with Latch](./concurrency-pattern-coordinate-with-latch.mdx) - Multi-fiber coordination" - }, - { - "id": "concurrency-pattern-race-timeout", - "title": "Concurrency Pattern 6: Race and Timeout Competing Effects", - "description": "Use race to compete effects and timeout to enforce deadlines, enabling cancellation when operations exceed time limits or complete.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates racing competing effects and handling timeouts.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\ninterface DataSource {\n readonly name: string;\n readonly latencyMs: number;\n}\n\n// Simulate fetching from different sources\nconst fetchFromSource = (source: DataSource): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[${source.name}] Starting fetch (latency: ${source.latencyMs}ms)`\n );\n\n yield* Effect.sleep(`${source.latencyMs} millis`);\n\n const result = `Data from ${source.name}`;\n\n yield* Effect.log(`[${source.name}] ✓ Completed`);\n\n return result;\n });\n\n// Main: demonstrate race patterns\nconst program = Effect.gen(function* () {\n console.log(`\\n[RACE] Competing effects with race and timeout\\n`);\n\n // Example 1: Simple race (fastest wins)\n console.log(`[1] Racing 3 data sources:\\n`);\n\n const sources: DataSource[] = [\n { name: \"Primary DC\", latencyMs: 200 },\n { name: \"Backup DC\", latencyMs: 150 },\n { name: \"Cache\", latencyMs: 50 },\n ];\n\n const raceResult = yield* Effect.race(\n fetchFromSource(sources[0]),\n Effect.race(fetchFromSource(sources[1]), fetchFromSource(sources[2]))\n );\n\n console.log(`\\nWinner: ${raceResult}\\n`);\n\n // Example 2: Timeout - succeed within deadline\n console.log(`[2] Timeout with fast operation:\\n`);\n\n const fastOp = fetchFromSource({ name: \"Fast Op\", latencyMs: 100 }).pipe(\n Effect.timeout(\"500 millis\")\n );\n\n const fastResult = yield* fastOp;\n\n console.log(`✓ Completed within timeout: ${fastResult}\\n`);\n\n // Example 3: Timeout - exceed deadline\n console.log(`[3] Timeout with slow operation:\\n`);\n\n const slowOp = fetchFromSource({ name: \"Slow Op\", latencyMs: 2000 }).pipe(\n Effect.timeout(\"500 millis\"),\n Effect.either\n );\n\n const timeoutResult = yield* slowOp;\n\n if (timeoutResult._tag === \"Left\") {\n console.log(`✗ Operation timed out after 500ms\\n`);\n }\n\n // Example 4: Race with timeout fallback\n console.log(`[4] Race with fallback on timeout:\\n`);\n\n const primary = fetchFromSource({ name: \"Primary\", latencyMs: 300 });\n\n const fallback = fetchFromSource({ name: \"Fallback\", latencyMs: 100 });\n\n const raceWithFallback = primary.pipe(\n Effect.timeout(\"150 millis\"),\n Effect.catchAll(() => {\n yield* Effect.log(`[PRIMARY] Timed out, using fallback`);\n\n return fallback;\n })\n );\n\n const fallbackResult = yield* raceWithFallback;\n\n console.log(`Result: ${fallbackResult}\\n`);\n\n // Example 5: Race all (collect all winners)\n console.log(`[5] Race all - multiple sources:\\n`);\n\n const raceAllResult = yield* Effect.raceAll(\n sources.map((s) =>\n fetchFromSource(s).pipe(\n Effect.map((data) => ({ source: s.name, data }))\n )\n )\n );\n\n console.log(`First to complete: ${raceAllResult.source}\\n`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Without race/timeout, competing effects create issues:\n\n- **Deadlocks**: Waiting for all to complete unnecessarily\n- **Hanging requests**: No deadline enforcement\n- **Wasted resources**: Slow operations continue indefinitely\n- **No fallback**: Can't switch to alternative on timeout\n\nRace/timeout enable:\n\n- **Fastest-wins**: Take first success\n- **Deadline enforcement**: Fail after time limit\n- **Resource cleanup**: Cancel slower operations\n- **Fallback patterns**: Alternative if primary times out\n\nReal-world example: Multi-datacenter request\n- **Without race**: Wait for slowest response\n- **With race**: Get response from fastest datacenter\n\n---", - "content": "## Guideline\n\nRace and timeout coordinate competing effects:\n\n- **race**: Multiple effects compete, first to succeed wins\n- **timeout**: Effect fails if not completed in time\n- **raceAll**: Race multiple effects, collect winners\n- **timeoutFail**: Fail with specific error on timeout\n\nPattern: `Effect.race(effect1, effect2)` or `effect.pipe(Effect.timeout(duration))`\n\n---\n\n## Rationale\n\nWithout race/timeout, competing effects create issues:\n\n- **Deadlocks**: Waiting for all to complete unnecessarily\n- **Hanging requests**: No deadline enforcement\n- **Wasted resources**: Slow operations continue indefinitely\n- **No fallback**: Can't switch to alternative on timeout\n\nRace/timeout enable:\n\n- **Fastest-wins**: Take first success\n- **Deadline enforcement**: Fail after time limit\n- **Resource cleanup**: Cancel slower operations\n- **Fallback patterns**: Alternative if primary times out\n\nReal-world example: Multi-datacenter request\n- **Without race**: Wait for slowest response\n- **With race**: Get response from fastest datacenter\n\n---\n\n## Good Example\n\nThis example demonstrates racing competing effects and handling timeouts.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\ninterface DataSource {\n readonly name: string;\n readonly latencyMs: number;\n}\n\n// Simulate fetching from different sources\nconst fetchFromSource = (source: DataSource): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[${source.name}] Starting fetch (latency: ${source.latencyMs}ms)`\n );\n\n yield* Effect.sleep(`${source.latencyMs} millis`);\n\n const result = `Data from ${source.name}`;\n\n yield* Effect.log(`[${source.name}] ✓ Completed`);\n\n return result;\n });\n\n// Main: demonstrate race patterns\nconst program = Effect.gen(function* () {\n console.log(`\\n[RACE] Competing effects with race and timeout\\n`);\n\n // Example 1: Simple race (fastest wins)\n console.log(`[1] Racing 3 data sources:\\n`);\n\n const sources: DataSource[] = [\n { name: \"Primary DC\", latencyMs: 200 },\n { name: \"Backup DC\", latencyMs: 150 },\n { name: \"Cache\", latencyMs: 50 },\n ];\n\n const raceResult = yield* Effect.race(\n fetchFromSource(sources[0]),\n Effect.race(fetchFromSource(sources[1]), fetchFromSource(sources[2]))\n );\n\n console.log(`\\nWinner: ${raceResult}\\n`);\n\n // Example 2: Timeout - succeed within deadline\n console.log(`[2] Timeout with fast operation:\\n`);\n\n const fastOp = fetchFromSource({ name: \"Fast Op\", latencyMs: 100 }).pipe(\n Effect.timeout(\"500 millis\")\n );\n\n const fastResult = yield* fastOp;\n\n console.log(`✓ Completed within timeout: ${fastResult}\\n`);\n\n // Example 3: Timeout - exceed deadline\n console.log(`[3] Timeout with slow operation:\\n`);\n\n const slowOp = fetchFromSource({ name: \"Slow Op\", latencyMs: 2000 }).pipe(\n Effect.timeout(\"500 millis\"),\n Effect.either\n );\n\n const timeoutResult = yield* slowOp;\n\n if (timeoutResult._tag === \"Left\") {\n console.log(`✗ Operation timed out after 500ms\\n`);\n }\n\n // Example 4: Race with timeout fallback\n console.log(`[4] Race with fallback on timeout:\\n`);\n\n const primary = fetchFromSource({ name: \"Primary\", latencyMs: 300 });\n\n const fallback = fetchFromSource({ name: \"Fallback\", latencyMs: 100 });\n\n const raceWithFallback = primary.pipe(\n Effect.timeout(\"150 millis\"),\n Effect.catchAll(() => {\n yield* Effect.log(`[PRIMARY] Timed out, using fallback`);\n\n return fallback;\n })\n );\n\n const fallbackResult = yield* raceWithFallback;\n\n console.log(`Result: ${fallbackResult}\\n`);\n\n // Example 5: Race all (collect all winners)\n console.log(`[5] Race all - multiple sources:\\n`);\n\n const raceAllResult = yield* Effect.raceAll(\n sources.map((s) =>\n fetchFromSource(s).pipe(\n Effect.map((data) => ({ source: s.name, data }))\n )\n )\n );\n\n console.log(`First to complete: ${raceAllResult.source}\\n`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Circuit Breaker with Timeout\n\nImplement circuit breaker using race:\n\n```typescript\nenum CircuitState {\n Closed = \"closed\",\n Open = \"open\",\n HalfOpen = \"half-open\",\n}\n\nconst circuitBreakerWithTimeout = (\n operation: Effect.Effect,\n timeoutMs: number,\n failureThreshold: number\n) =>\n Effect.gen(function* () {\n let state = CircuitState.Closed;\n let failureCount = 0;\n let successCount = 0;\n let lastOpenTime = 0;\n\n return (op: Effect.Effect) =>\n Effect.gen(function* () {\n // Check circuit state\n if (state === CircuitState.Open) {\n const timeSinceOpen = Date.now() - lastOpenTime;\n\n if (timeSinceOpen > 5000) {\n // Try half-open\n state = CircuitState.HalfOpen;\n yield* Effect.log(`[CIRCUIT] Trying half-open`);\n } else {\n yield* Effect.fail(\n new Error(\"Circuit breaker open\")\n );\n }\n }\n\n // Execute with timeout\n const result = yield* op.pipe(\n Effect.timeout(`${timeoutMs} millis`),\n Effect.either\n );\n\n if (result._tag === \"Right\") {\n failureCount = 0;\n\n if (state === CircuitState.HalfOpen) {\n successCount++;\n\n if (successCount >= 3) {\n state = CircuitState.Closed;\n yield* Effect.log(\n `[CIRCUIT] Closed (recovered)`\n );\n }\n }\n\n return result.right;\n }\n\n // Failure\n failureCount++;\n\n if (failureCount >= failureThreshold) {\n state = CircuitState.Open;\n lastOpenTime = Date.now();\n\n yield* Effect.log(\n `[CIRCUIT] Opened (too many failures)`\n );\n }\n\n yield* Effect.fail(result.left);\n });\n });\n```\n\n---\n\n## Advanced: Retry with Timeout and Backoff\n\nCombine retry, timeout, and backoff:\n\n```typescript\nconst retryWithTimeoutAndBackoff = (\n effect: Effect.Effect,\n config: {\n timeoutMs: number;\n maxRetries: number;\n initialBackoffMs: number;\n }\n): Effect.Effect =>\n Effect.gen(function* () {\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < config.maxRetries; attempt++) {\n const result = yield* effect.pipe(\n Effect.timeout(`${config.timeoutMs} millis`),\n Effect.either\n );\n\n if (result._tag === \"Right\") {\n return result.right;\n }\n\n lastError = result.left as Error;\n\n if (attempt < config.maxRetries - 1) {\n const backoff = config.initialBackoffMs * Math.pow(2, attempt);\n\n yield* Effect.log(\n `[RETRY] Attempt ${attempt + 1} failed, waiting ${backoff}ms`\n );\n\n yield* Effect.sleep(`${backoff} millis`);\n }\n }\n\n yield* Effect.fail(\n lastError || new Error(\"All retries exhausted\")\n );\n });\n\n// Usage\nconst resilientFetch = retryWithTimeoutAndBackoff(\n Effect.gen(function* () {\n // Simulated fetch\n yield* Effect.sleep(\"100 millis\");\n return \"data\";\n }),\n {\n timeoutMs: 500,\n maxRetries: 3,\n initialBackoffMs: 100,\n }\n);\n```\n\n---\n\n## Advanced: Timeout with Graceful Shutdown\n\nCancel and cleanup on timeout:\n\n```typescript\nconst timeoutWithCleanup = (\n effect: Effect.Effect,\n timeoutMs: number,\n cleanup: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const result = yield* effect.pipe(\n Effect.timeout(`${timeoutMs} millis`),\n Effect.ensuring(cleanup),\n Effect.either\n );\n\n if (result._tag === \"Left\") {\n yield* Effect.fail(\n new Error(`Operation timed out after ${timeoutMs}ms`)\n );\n }\n\n return result.right;\n });\n\n// Usage: Timeout with resource cleanup\nconst operation = timeoutWithCleanup(\n Effect.gen(function* () {\n yield* Effect.sleep(\"2000 millis\");\n return \"done\";\n }),\n 500,\n Effect.log(`[CLEANUP] Releasing resources`)\n);\n```\n\n---\n\n## Advanced: First to Succeed Pattern\n\nRace effects until one succeeds:\n\n```typescript\nconst firstToSucceed = (\n effects: Effect.Effect[]\n): Effect.Effect =>\n Effect.gen(function* () {\n if (effects.length === 0) {\n yield* Effect.fail(new Error(\"No effects to race\"));\n }\n\n let lastError: Error | undefined;\n\n for (const effect of effects) {\n const result = yield* effect.pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n return result.right;\n }\n\n lastError = result.left as Error;\n }\n\n yield* Effect.fail(\n lastError || new Error(\"All effects failed\")\n );\n });\n\n// Usage: Try multiple sources\nconst data = firstToSucceed([\n fetchFromSource({ name: \"Source 1\", latencyMs: 100 }),\n fetchFromSource({ name: \"Source 2\", latencyMs: 50 }),\n fetchFromSource({ name: \"Source 3\", latencyMs: 75 }),\n]);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use race when:**\n\n- Multiple sources available, want fastest\n- Timeout deadlines necessary\n- Fallback behavior needed\n- Competitive execution\n- Resource constraints\n\n✅ **Use timeout when:**\n\n- API calls to external services\n- User-facing operations\n- Preventing resource exhaustion\n- Enforcing SLA limits\n\n⚠️ **Trade-offs:**\n\n- Race loses slower operations (resource waste)\n- Timeout errors can mask underlying issues\n- Resource cleanup must be explicit\n- Complexity increases with many competing effects\n\n---\n\n## Common Patterns\n\n| Pattern | Use Case | Code |\n| --- | --- | --- |\n| **Race** | Fastest wins | `Effect.race(a, b)` |\n| **Timeout** | Enforce deadline | `Effect.timeout(\"5s\")` |\n| **Fallback** | Try primary, use secondary | `primary.pipe(Effect.catch(() => secondary))` |\n| **First to Succeed** | Try until one works | `firstToSucceed([a, b, c])` |\n\n---\n\n## See Also\n\n- [Concurrency Pattern 3: Coordinate with Latch](./concurrency-pattern-coordinate-with-latch.mdx) - Multi-fiber coordination\n- [Scheduling Pattern 2: Exponential Backoff](./scheduling-pattern-exponential-backoff.mdx) - Retry with backoff\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background execution\n- [Concurrency Pattern 2: Rate Limit with Semaphore](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Resource limiting" - }, - { - "id": "combinator-conditional", - "title": "Conditional Branching with if, when, and cond", - "description": "Use combinators such as if, when, and cond to branch computations based on runtime conditions, without imperative if statements.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Branch based on a condition\nconst effect = Effect.if(true, {\n onTrue: () => Effect.succeed(\"yes\"),\n onFalse: () => Effect.succeed(\"no\"),\n}); // Effect\n\n// Option: Conditionally create an Option\nconst option = true ? Option.some(\"yes\") : Option.none(); // Option (Some(\"yes\"))\n\n// Either: Conditionally create an Either\nconst either = true ? Either.right(\"yes\") : Either.left(\"error\"); // Either (Right(\"yes\"))\n\n// Stream: Conditionally emit a stream\nconst stream = false ? Stream.fromIterable([1, 2]) : Stream.empty; // Stream (empty)\n```\n\n**Explanation:** \nThese combinators let you branch your computation based on a boolean or predicate, without leaving the world of composable, type-safe code. \nYou can also use `when` to run an effect only if a condition is true, or `unless` to run it only if a condition is false.", - "antiPattern": "Using imperative `if` statements to decide which effect, option, either, or stream to return, breaking composability and making error/context handling less predictable.", - "explanation": "Declarative branching keeps your code composable, testable, and easy to reason about. \nIt also ensures that error handling and context propagation are preserved, and that your code remains consistent across different Effect types.", - "content": "# Conditional Branching with `if`, `when`, and `cond`\n\n## Guideline\n\nUse combinators like `if`, `when`, and `cond` to express conditional logic in a declarative, composable way. \nThese combinators allow you to branch computations based on runtime conditions, without resorting to imperative `if` statements.\n\n## Rationale\n\nDeclarative branching keeps your code composable, testable, and easy to reason about. \nIt also ensures that error handling and context propagation are preserved, and that your code remains consistent across different Effect types.\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Branch based on a condition\nconst effect = Effect.if(true, {\n onTrue: () => Effect.succeed(\"yes\"),\n onFalse: () => Effect.succeed(\"no\"),\n}); // Effect\n\n// Option: Conditionally create an Option\nconst option = true ? Option.some(\"yes\") : Option.none(); // Option (Some(\"yes\"))\n\n// Either: Conditionally create an Either\nconst either = true ? Either.right(\"yes\") : Either.left(\"error\"); // Either (Right(\"yes\"))\n\n// Stream: Conditionally emit a stream\nconst stream = false ? Stream.fromIterable([1, 2]) : Stream.empty; // Stream (empty)\n```\n\n**Explanation:** \nThese combinators let you branch your computation based on a boolean or predicate, without leaving the world of composable, type-safe code. \nYou can also use `when` to run an effect only if a condition is true, or `unless` to run it only if a condition is false.\n\n## Anti-Pattern\n\nUsing imperative `if` statements to decide which effect, option, either, or stream to return, breaking composability and making error/context handling less predictable." - }, - { - "id": "conditionally-branching-workflows", - "title": "Conditionally Branching Workflows", - "description": "Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "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.\n\n```typescript\nimport { Effect } from \"effect\";\n\ninterface User {\n id: number;\n status: \"active\" | \"inactive\";\n roles: string[];\n}\n\ntype UserError = \"DbError\" | \"UserIsInactive\" | \"UserIsNotAdmin\";\n\nconst findUser = (id: number): Effect.Effect =>\n Effect.succeed({ id, status: \"active\", roles: [\"admin\"] });\n\n// Reusable, testable predicates that document business rules.\nconst isActive = (user: User): boolean => user.status === \"active\";\n\nconst isAdmin = (user: User): boolean => user.roles.includes(\"admin\");\n\nconst program = (id: number): Effect.Effect =>\n findUser(id).pipe(\n // Validate user is active using Effect.filterOrFail\n Effect.filterOrFail(isActive, () => \"UserIsInactive\" as const),\n // Validate user is admin using Effect.filterOrFail\n Effect.filterOrFail(isAdmin, () => \"UserIsNotAdmin\" as const),\n // Success case\n Effect.map((user) => `Welcome, admin user #${user.id}!`)\n );\n\n// We can then handle the specific failures in a type-safe way.\nconst handled = program(123).pipe(\n Effect.match({\n onFailure: (error) => {\n switch (error) {\n case \"UserIsNotAdmin\":\n return \"Access denied: requires admin role.\";\n case \"UserIsInactive\":\n return \"Access denied: user is not active.\";\n case \"DbError\":\n return \"Error: could not find user.\";\n default:\n return `Unknown error: ${error}`;\n }\n },\n onSuccess: (result) => result,\n })\n);\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* handled;\n yield* Effect.log(result);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "Using `Effect.flatMap` with a manual `if` statement and forgetting to handle the `else` case. This is a common mistake that leads to an inferred type of `Effect`, which can cause confusing type errors downstream because the success value is lost.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { findUser, isAdmin } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: The `else` case is missing.\nconst program = (id: number) =>\n findUser(id).pipe(\n Effect.flatMap((user) => {\n if (isAdmin(user)) {\n // This returns Effect, but what happens if the user is not an admin?\n return Effect.succeed(user);\n }\n // Because there's no `else` branch, TypeScript infers that this\n // block can also implicitly return `void`.\n // The resulting type is Effect, which is problematic.\n }),\n // This `map` will now have a type error because `u` could be `void`.\n Effect.map((u) => `Welcome, ${u.name}!`)\n );\n\n// `Effect.filterOrFail` avoids this problem entirely by forcing a failure,\n// which keeps the success channel clean and correctly typed.\n```\n\n### Why This is Better\n\n- **It's a Real Bug:** This isn't just a style issue; it's a legitimate logical error that leads to incorrect types and broken code.\n- **It's a Common Mistake:** Developers new to functional pipelines often forget that every path must return a value.\n- **It Reinforces the \"Why\":** It perfectly demonstrates _why_ `Effect.filterOrFail` is superior: `filterOrFail` guarantees that if the condition fails, the computation fails, preserving the integrity of the success channel.", - "explanation": "This pattern allows you to embed decision-making logic directly into your composition pipelines, making your code more declarative and readable. It solves two key problems:\n\n1. **Separation of Concerns:** It cleanly separates the logic of producing a value from the logic of validating or making decisions about that value.\n2. **Reusable Business Logic:** A predicate function (e.g., `const isAdmin = (user: User) => ...`) becomes a named, reusable, and testable piece of business logic, far superior to scattering inline `if` statements throughout your code.\n\nUsing these operators turns conditional logic into a composable part of your `Effect`, rather than an imperative statement that breaks the flow.\n\n---", - "content": "### Pattern: `conditionally-branching-workflows.mdx`\n\n## Guideline\n\nTo make decisions based on a successful value within an `Effect` pipeline, use predicate-based operators:\n\n- **To Validate and Fail:** Use `Effect.filterOrFail(predicate, onFailure)` to stop the workflow if a condition is not met.\n- **To Choose a Path:** Use `Effect.if(condition, { onTrue, onFalse })` or `Effect.gen` to execute different effects based on a condition.\n\n---\n\n## Rationale\n\nThis pattern allows you to embed decision-making logic directly into your composition pipelines, making your code more declarative and readable. It solves two key problems:\n\n1. **Separation of Concerns:** It cleanly separates the logic of producing a value from the logic of validating or making decisions about that value.\n2. **Reusable Business Logic:** A predicate function (e.g., `const isAdmin = (user: User) => ...`) becomes a named, reusable, and testable piece of business logic, far superior to scattering inline `if` statements throughout your code.\n\nUsing these operators turns conditional logic into a composable part of your `Effect`, rather than an imperative statement that breaks the flow.\n\n---\n\n## Good Example: Validating a User\n\nHere, 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.\n\n```typescript\nimport { Effect } from \"effect\";\n\ninterface User {\n id: number;\n status: \"active\" | \"inactive\";\n roles: string[];\n}\n\ntype UserError = \"DbError\" | \"UserIsInactive\" | \"UserIsNotAdmin\";\n\nconst findUser = (id: number): Effect.Effect =>\n Effect.succeed({ id, status: \"active\", roles: [\"admin\"] });\n\n// Reusable, testable predicates that document business rules.\nconst isActive = (user: User): boolean => user.status === \"active\";\n\nconst isAdmin = (user: User): boolean => user.roles.includes(\"admin\");\n\nconst program = (id: number): Effect.Effect =>\n findUser(id).pipe(\n // Validate user is active using Effect.filterOrFail\n Effect.filterOrFail(isActive, () => \"UserIsInactive\" as const),\n // Validate user is admin using Effect.filterOrFail\n Effect.filterOrFail(isAdmin, () => \"UserIsNotAdmin\" as const),\n // Success case\n Effect.map((user) => `Welcome, admin user #${user.id}!`)\n );\n\n// We can then handle the specific failures in a type-safe way.\nconst handled = program(123).pipe(\n Effect.match({\n onFailure: (error) => {\n switch (error) {\n case \"UserIsNotAdmin\":\n return \"Access denied: requires admin role.\";\n case \"UserIsInactive\":\n return \"Access denied: user is not active.\";\n case \"DbError\":\n return \"Error: could not find user.\";\n default:\n return `Unknown error: ${error}`;\n }\n },\n onSuccess: (result) => result,\n })\n);\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* handled;\n yield* Effect.log(result);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nUsing `Effect.flatMap` with a manual `if` statement and forgetting to handle the `else` case. This is a common mistake that leads to an inferred type of `Effect`, which can cause confusing type errors downstream because the success value is lost.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { findUser, isAdmin } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: The `else` case is missing.\nconst program = (id: number) =>\n findUser(id).pipe(\n Effect.flatMap((user) => {\n if (isAdmin(user)) {\n // This returns Effect, but what happens if the user is not an admin?\n return Effect.succeed(user);\n }\n // Because there's no `else` branch, TypeScript infers that this\n // block can also implicitly return `void`.\n // The resulting type is Effect, which is problematic.\n }),\n // This `map` will now have a type error because `u` could be `void`.\n Effect.map((u) => `Welcome, ${u.name}!`)\n );\n\n// `Effect.filterOrFail` avoids this problem entirely by forcing a failure,\n// which keeps the success channel clean and correctly typed.\n```\n\n### Why This is Better\n\n- **It's a Real Bug:** This isn't just a style issue; it's a legitimate logical error that leads to incorrect types and broken code.\n- **It's a Common Mistake:** Developers new to functional pipelines often forget that every path must return a value.\n- **It Reinforces the \"Why\":** It perfectly demonstrates _why_ `Effect.filterOrFail` is superior: `filterOrFail` guarantees that if the condition fails, the computation fails, preserving the integrity of the success channel." - }, - { - "id": "api-cors", - "title": "Configure CORS for APIs", - "description": "Configure CORS headers to allow legitimate cross-origin requests while blocking unauthorized ones.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "example": "```typescript\nimport { Effect } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. CORS configuration\n// ============================================\n\ninterface CorsConfig {\n readonly allowedOrigins: ReadonlyArray | \"*\"\n readonly allowedMethods: ReadonlyArray\n readonly allowedHeaders: ReadonlyArray\n readonly exposedHeaders?: ReadonlyArray\n readonly credentials?: boolean\n readonly maxAge?: number\n}\n\nconst defaultCorsConfig: CorsConfig = {\n allowedOrigins: \"*\",\n allowedMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Request-Id\"],\n exposedHeaders: [\"X-Request-Id\", \"X-Response-Time\"],\n credentials: false,\n maxAge: 86400, // 24 hours\n}\n\n// ============================================\n// 2. Check if origin is allowed\n// ============================================\n\nconst isOriginAllowed = (\n origin: string | undefined,\n allowedOrigins: ReadonlyArray | \"*\"\n): boolean => {\n if (!origin) return false\n if (allowedOrigins === \"*\") return true\n return allowedOrigins.includes(origin)\n}\n\n// ============================================\n// 3. Add CORS headers to response\n// ============================================\n\nconst addCorsHeaders = (\n response: HttpServerResponse.HttpServerResponse,\n origin: string | undefined,\n config: CorsConfig\n): HttpServerResponse.HttpServerResponse => {\n let result = response\n\n // Set allowed origin\n if (config.allowedOrigins === \"*\") {\n result = HttpServerResponse.setHeader(result, \"Access-Control-Allow-Origin\", \"*\")\n } else if (origin && isOriginAllowed(origin, config.allowedOrigins)) {\n result = HttpServerResponse.setHeader(result, \"Access-Control-Allow-Origin\", origin)\n result = HttpServerResponse.setHeader(result, \"Vary\", \"Origin\")\n }\n\n // Set allowed methods\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Methods\",\n config.allowedMethods.join(\", \")\n )\n\n // Set allowed headers\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Headers\",\n config.allowedHeaders.join(\", \")\n )\n\n // Set exposed headers\n if (config.exposedHeaders?.length) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Expose-Headers\",\n config.exposedHeaders.join(\", \")\n )\n }\n\n // Set credentials\n if (config.credentials) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Credentials\",\n \"true\"\n )\n }\n\n // Set max age for preflight cache\n if (config.maxAge) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Max-Age\",\n String(config.maxAge)\n )\n }\n\n return result\n}\n\n// ============================================\n// 4. CORS middleware\n// ============================================\n\nconst withCors = (config: CorsConfig = defaultCorsConfig) =>\n (\n handler: Effect.Effect\n ): Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n E,\n R | HttpServerRequest.HttpServerRequest\n > =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const origin = request.headers[\"origin\"]\n\n // Handle preflight OPTIONS request\n if (request.method === \"OPTIONS\") {\n const preflightResponse = HttpServerResponse.empty({ status: 204 })\n return addCorsHeaders(preflightResponse, origin, config)\n }\n\n // Check if origin is allowed\n if (\n origin &&\n config.allowedOrigins !== \"*\" &&\n !isOriginAllowed(origin, config.allowedOrigins)\n ) {\n return HttpServerResponse.json(\n { error: \"CORS: Origin not allowed\" },\n { status: 403 }\n )\n }\n\n // Process request and add CORS headers to response\n const response = yield* handler\n return addCorsHeaders(response, origin, config)\n })\n\n// ============================================\n// 5. Usage examples\n// ============================================\n\n// Allow all origins (development)\nconst devCors = withCors({\n ...defaultCorsConfig,\n allowedOrigins: \"*\",\n})\n\n// Specific origins (production)\nconst prodCors = withCors({\n allowedOrigins: [\n \"https://myapp.com\",\n \"https://admin.myapp.com\",\n ],\n allowedMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\"],\n allowedHeaders: [\"Content-Type\", \"Authorization\"],\n credentials: true,\n maxAge: 3600,\n})\n\n// Apply to handlers\nconst myHandler = Effect.succeed(\n HttpServerResponse.json({ message: \"Hello!\" })\n)\n\nconst corsEnabledHandler = devCors(myHandler)\n```", - "antiPattern": "", - "explanation": "Browsers block cross-origin requests by default:\n\n1. **Security** - Prevents malicious sites from accessing your API\n2. **Controlled access** - Allow specific origins only\n3. **Credentials** - Control cookie/auth header sharing\n4. **Methods** - Limit which HTTP methods are allowed\n\n---", - "content": "## Guideline\n\nImplement CORS as middleware that adds appropriate headers and handles preflight OPTIONS requests.\n\n---\n\n## Rationale\n\nBrowsers block cross-origin requests by default:\n\n1. **Security** - Prevents malicious sites from accessing your API\n2. **Controlled access** - Allow specific origins only\n3. **Credentials** - Control cookie/auth header sharing\n4. **Methods** - Limit which HTTP methods are allowed\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\"\nimport { HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. CORS configuration\n// ============================================\n\ninterface CorsConfig {\n readonly allowedOrigins: ReadonlyArray | \"*\"\n readonly allowedMethods: ReadonlyArray\n readonly allowedHeaders: ReadonlyArray\n readonly exposedHeaders?: ReadonlyArray\n readonly credentials?: boolean\n readonly maxAge?: number\n}\n\nconst defaultCorsConfig: CorsConfig = {\n allowedOrigins: \"*\",\n allowedMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n allowedHeaders: [\"Content-Type\", \"Authorization\", \"X-Request-Id\"],\n exposedHeaders: [\"X-Request-Id\", \"X-Response-Time\"],\n credentials: false,\n maxAge: 86400, // 24 hours\n}\n\n// ============================================\n// 2. Check if origin is allowed\n// ============================================\n\nconst isOriginAllowed = (\n origin: string | undefined,\n allowedOrigins: ReadonlyArray | \"*\"\n): boolean => {\n if (!origin) return false\n if (allowedOrigins === \"*\") return true\n return allowedOrigins.includes(origin)\n}\n\n// ============================================\n// 3. Add CORS headers to response\n// ============================================\n\nconst addCorsHeaders = (\n response: HttpServerResponse.HttpServerResponse,\n origin: string | undefined,\n config: CorsConfig\n): HttpServerResponse.HttpServerResponse => {\n let result = response\n\n // Set allowed origin\n if (config.allowedOrigins === \"*\") {\n result = HttpServerResponse.setHeader(result, \"Access-Control-Allow-Origin\", \"*\")\n } else if (origin && isOriginAllowed(origin, config.allowedOrigins)) {\n result = HttpServerResponse.setHeader(result, \"Access-Control-Allow-Origin\", origin)\n result = HttpServerResponse.setHeader(result, \"Vary\", \"Origin\")\n }\n\n // Set allowed methods\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Methods\",\n config.allowedMethods.join(\", \")\n )\n\n // Set allowed headers\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Headers\",\n config.allowedHeaders.join(\", \")\n )\n\n // Set exposed headers\n if (config.exposedHeaders?.length) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Expose-Headers\",\n config.exposedHeaders.join(\", \")\n )\n }\n\n // Set credentials\n if (config.credentials) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Allow-Credentials\",\n \"true\"\n )\n }\n\n // Set max age for preflight cache\n if (config.maxAge) {\n result = HttpServerResponse.setHeader(\n result,\n \"Access-Control-Max-Age\",\n String(config.maxAge)\n )\n }\n\n return result\n}\n\n// ============================================\n// 4. CORS middleware\n// ============================================\n\nconst withCors = (config: CorsConfig = defaultCorsConfig) =>\n (\n handler: Effect.Effect\n ): Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n E,\n R | HttpServerRequest.HttpServerRequest\n > =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const origin = request.headers[\"origin\"]\n\n // Handle preflight OPTIONS request\n if (request.method === \"OPTIONS\") {\n const preflightResponse = HttpServerResponse.empty({ status: 204 })\n return addCorsHeaders(preflightResponse, origin, config)\n }\n\n // Check if origin is allowed\n if (\n origin &&\n config.allowedOrigins !== \"*\" &&\n !isOriginAllowed(origin, config.allowedOrigins)\n ) {\n return HttpServerResponse.json(\n { error: \"CORS: Origin not allowed\" },\n { status: 403 }\n )\n }\n\n // Process request and add CORS headers to response\n const response = yield* handler\n return addCorsHeaders(response, origin, config)\n })\n\n// ============================================\n// 5. Usage examples\n// ============================================\n\n// Allow all origins (development)\nconst devCors = withCors({\n ...defaultCorsConfig,\n allowedOrigins: \"*\",\n})\n\n// Specific origins (production)\nconst prodCors = withCors({\n allowedOrigins: [\n \"https://myapp.com\",\n \"https://admin.myapp.com\",\n ],\n allowedMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\"],\n allowedHeaders: [\"Content-Type\", \"Authorization\"],\n credentials: true,\n maxAge: 3600,\n})\n\n// Apply to handlers\nconst myHandler = Effect.succeed(\n HttpServerResponse.json({ message: \"Hello!\" })\n)\n\nconst corsEnabledHandler = devCors(myHandler)\n```\n\n## CORS Headers\n\n| Header | Purpose |\n|--------|---------|\n| `Access-Control-Allow-Origin` | Which origins can access |\n| `Access-Control-Allow-Methods` | Which HTTP methods |\n| `Access-Control-Allow-Headers` | Which request headers |\n| `Access-Control-Expose-Headers` | Which response headers visible |\n| `Access-Control-Allow-Credentials` | Allow cookies/auth |\n| `Access-Control-Max-Age` | Preflight cache time |\n\n## Preflight Requests\n\nBrowsers send OPTIONS preflight for:\n- Non-simple methods (PUT, DELETE, etc.)\n- Custom headers\n- Content-Type other than form data\n\n## Common Configurations\n\n| Scenario | Configuration |\n|----------|---------------|\n| Public API | `allowedOrigins: \"*\"`, no credentials |\n| Web app | Specific origins, credentials |\n| Internal | Localhost + internal domains |\n\n## Security Tips\n\n1. **Never use `*` with credentials** - Browser will reject\n2. **Be specific** - List exact origins in production\n3. **Limit methods** - Only allow what you need\n4. **Audit headers** - Don't expose sensitive headers" - }, - { - "id": "tooling-linting", - "title": "Configure Linting for Effect", - "description": "Use Biome for fast linting with Effect-friendly configuration.", - "skillLevel": "intermediate", - "useCases": [ - "tooling-and-debugging" - ], - "example": "### 1. Biome Configuration (Recommended)\n\n```json\n// biome.json\n{\n \"$schema\": \"https://biomejs.dev/schemas/1.8.0/schema.json\",\n \"organizeImports\": {\n \"enabled\": true\n },\n \"linter\": {\n \"enabled\": true,\n \"rules\": {\n \"recommended\": true,\n \"complexity\": {\n \"noExcessiveCognitiveComplexity\": \"warn\",\n \"noForEach\": \"off\", // Effect uses forEach patterns\n \"useLiteralKeys\": \"off\" // Effect uses computed keys\n },\n \"correctness\": {\n \"noUnusedVariables\": \"error\",\n \"noUnusedImports\": \"error\",\n \"useExhaustiveDependencies\": \"warn\"\n },\n \"style\": {\n \"noNonNullAssertion\": \"warn\",\n \"useConst\": \"error\",\n \"noParameterAssign\": \"error\"\n },\n \"suspicious\": {\n \"noExplicitAny\": \"warn\",\n \"noConfusingVoidType\": \"off\" // Effect uses void\n },\n \"nursery\": {\n \"noRestrictedImports\": {\n \"level\": \"error\",\n \"options\": {\n \"paths\": {\n \"lodash\": \"Use Effect functions instead\",\n \"ramda\": \"Use Effect functions instead\"\n }\n }\n }\n }\n }\n },\n \"formatter\": {\n \"enabled\": true,\n \"indentStyle\": \"space\",\n \"indentWidth\": 2,\n \"lineWidth\": 100\n },\n \"javascript\": {\n \"formatter\": {\n \"semicolons\": \"asNeeded\",\n \"quoteStyle\": \"double\",\n \"trailingComma\": \"es5\"\n }\n },\n \"files\": {\n \"ignore\": [\n \"node_modules\",\n \"dist\",\n \"coverage\",\n \"*.gen.ts\"\n ]\n }\n}\n```\n\n### 2. ESLint Configuration (Alternative)\n\n```javascript\n// eslint.config.js\nimport eslint from \"@eslint/js\"\nimport tseslint from \"typescript-eslint\"\n\nexport default tseslint.config(\n eslint.configs.recommended,\n ...tseslint.configs.strictTypeChecked,\n {\n languageOptions: {\n parserOptions: {\n project: true,\n tsconfigRootDir: import.meta.dirname,\n },\n },\n rules: {\n // TypeScript strict rules\n \"@typescript-eslint/no-unused-vars\": [\n \"error\",\n { argsIgnorePattern: \"^_\" }\n ],\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n \"@typescript-eslint/explicit-function-return-type\": \"off\",\n \"@typescript-eslint/no-floating-promises\": \"error\",\n\n // Effect-friendly rules\n \"@typescript-eslint/no-confusing-void-expression\": \"off\",\n \"@typescript-eslint/no-misused-promises\": [\n \"error\",\n { checksVoidReturn: false }\n ],\n\n // Style rules\n \"prefer-const\": \"error\",\n \"no-var\": \"error\",\n \"object-shorthand\": \"error\",\n \"prefer-template\": \"error\",\n },\n },\n {\n files: [\"**/*.test.ts\"],\n rules: {\n \"@typescript-eslint/no-explicit-any\": \"off\",\n },\n },\n {\n ignores: [\"dist/\", \"coverage/\", \"node_modules/\"],\n }\n)\n```\n\n### 3. Package.json Scripts\n\n```json\n{\n \"scripts\": {\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --apply .\",\n \"lint:ci\": \"biome ci .\",\n \"format\": \"biome format --write .\",\n \"format:check\": \"biome format .\"\n }\n}\n```\n\n### 4. VS Code Integration\n\n```json\n// .vscode/settings.json\n{\n \"editor.defaultFormatter\": \"biomejs.biome\",\n \"editor.formatOnSave\": true,\n \"editor.codeActionsOnSave\": {\n \"quickfix.biome\": \"explicit\",\n \"source.organizeImports.biome\": \"explicit\"\n },\n \"[typescript]\": {\n \"editor.defaultFormatter\": \"biomejs.biome\"\n },\n \"[typescriptreact]\": {\n \"editor.defaultFormatter\": \"biomejs.biome\"\n }\n}\n```\n\n### 5. Pre-commit Hook\n\n```json\n// package.json\n{\n \"scripts\": {\n \"prepare\": \"husky\"\n }\n}\n```\n\n```bash\n# .husky/pre-commit\nbun run lint:ci\nbun run typecheck\n```\n\n### 6. Effect-Specific Rules to Consider\n\n```typescript\n// Custom rules you might want\n\n// ❌ Bad: Using Promise where Effect should be used\nconst fetchData = async () => { } // Warn in Effect codebase\n\n// ✅ Good: Using Effect\nconst fetchData = Effect.gen(function* () { })\n\n// ❌ Bad: Throwing errors\nconst validate = (x: unknown) => {\n if (!x) throw new Error(\"Invalid\") // Error\n}\n\n// ✅ Good: Returning Effect with error\nconst validate = (x: unknown) =>\n x ? Effect.succeed(x) : Effect.fail(new ValidationError())\n\n// ❌ Bad: Using null/undefined directly\nconst maybeValue: string | null = null // Warn\n\n// ✅ Good: Using Option\nconst maybeValue: Option.Option = Option.none()\n```", - "antiPattern": "", - "explanation": "Good linting for Effect:\n\n1. **Catches errors** - Unused variables, missing awaits\n2. **Enforces style** - Consistent code across team\n3. **Avoids antipatterns** - No implicit any, proper typing\n4. **Fast feedback** - Errors in editor immediately\n\n---", - "content": "## Guideline\n\nConfigure Biome (recommended) or ESLint with rules that work well with Effect's functional patterns.\n\n---\n\n## Rationale\n\nGood linting for Effect:\n\n1. **Catches errors** - Unused variables, missing awaits\n2. **Enforces style** - Consistent code across team\n3. **Avoids antipatterns** - No implicit any, proper typing\n4. **Fast feedback** - Errors in editor immediately\n\n---\n\n## Good Example\n\n### 1. Biome Configuration (Recommended)\n\n```json\n// biome.json\n{\n \"$schema\": \"https://biomejs.dev/schemas/1.8.0/schema.json\",\n \"organizeImports\": {\n \"enabled\": true\n },\n \"linter\": {\n \"enabled\": true,\n \"rules\": {\n \"recommended\": true,\n \"complexity\": {\n \"noExcessiveCognitiveComplexity\": \"warn\",\n \"noForEach\": \"off\", // Effect uses forEach patterns\n \"useLiteralKeys\": \"off\" // Effect uses computed keys\n },\n \"correctness\": {\n \"noUnusedVariables\": \"error\",\n \"noUnusedImports\": \"error\",\n \"useExhaustiveDependencies\": \"warn\"\n },\n \"style\": {\n \"noNonNullAssertion\": \"warn\",\n \"useConst\": \"error\",\n \"noParameterAssign\": \"error\"\n },\n \"suspicious\": {\n \"noExplicitAny\": \"warn\",\n \"noConfusingVoidType\": \"off\" // Effect uses void\n },\n \"nursery\": {\n \"noRestrictedImports\": {\n \"level\": \"error\",\n \"options\": {\n \"paths\": {\n \"lodash\": \"Use Effect functions instead\",\n \"ramda\": \"Use Effect functions instead\"\n }\n }\n }\n }\n }\n },\n \"formatter\": {\n \"enabled\": true,\n \"indentStyle\": \"space\",\n \"indentWidth\": 2,\n \"lineWidth\": 100\n },\n \"javascript\": {\n \"formatter\": {\n \"semicolons\": \"asNeeded\",\n \"quoteStyle\": \"double\",\n \"trailingComma\": \"es5\"\n }\n },\n \"files\": {\n \"ignore\": [\n \"node_modules\",\n \"dist\",\n \"coverage\",\n \"*.gen.ts\"\n ]\n }\n}\n```\n\n### 2. ESLint Configuration (Alternative)\n\n```javascript\n// eslint.config.js\nimport eslint from \"@eslint/js\"\nimport tseslint from \"typescript-eslint\"\n\nexport default tseslint.config(\n eslint.configs.recommended,\n ...tseslint.configs.strictTypeChecked,\n {\n languageOptions: {\n parserOptions: {\n project: true,\n tsconfigRootDir: import.meta.dirname,\n },\n },\n rules: {\n // TypeScript strict rules\n \"@typescript-eslint/no-unused-vars\": [\n \"error\",\n { argsIgnorePattern: \"^_\" }\n ],\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n \"@typescript-eslint/explicit-function-return-type\": \"off\",\n \"@typescript-eslint/no-floating-promises\": \"error\",\n\n // Effect-friendly rules\n \"@typescript-eslint/no-confusing-void-expression\": \"off\",\n \"@typescript-eslint/no-misused-promises\": [\n \"error\",\n { checksVoidReturn: false }\n ],\n\n // Style rules\n \"prefer-const\": \"error\",\n \"no-var\": \"error\",\n \"object-shorthand\": \"error\",\n \"prefer-template\": \"error\",\n },\n },\n {\n files: [\"**/*.test.ts\"],\n rules: {\n \"@typescript-eslint/no-explicit-any\": \"off\",\n },\n },\n {\n ignores: [\"dist/\", \"coverage/\", \"node_modules/\"],\n }\n)\n```\n\n### 3. Package.json Scripts\n\n```json\n{\n \"scripts\": {\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --apply .\",\n \"lint:ci\": \"biome ci .\",\n \"format\": \"biome format --write .\",\n \"format:check\": \"biome format .\"\n }\n}\n```\n\n### 4. VS Code Integration\n\n```json\n// .vscode/settings.json\n{\n \"editor.defaultFormatter\": \"biomejs.biome\",\n \"editor.formatOnSave\": true,\n \"editor.codeActionsOnSave\": {\n \"quickfix.biome\": \"explicit\",\n \"source.organizeImports.biome\": \"explicit\"\n },\n \"[typescript]\": {\n \"editor.defaultFormatter\": \"biomejs.biome\"\n },\n \"[typescriptreact]\": {\n \"editor.defaultFormatter\": \"biomejs.biome\"\n }\n}\n```\n\n### 5. Pre-commit Hook\n\n```json\n// package.json\n{\n \"scripts\": {\n \"prepare\": \"husky\"\n }\n}\n```\n\n```bash\n# .husky/pre-commit\nbun run lint:ci\nbun run typecheck\n```\n\n### 6. Effect-Specific Rules to Consider\n\n```typescript\n// Custom rules you might want\n\n// ❌ Bad: Using Promise where Effect should be used\nconst fetchData = async () => { } // Warn in Effect codebase\n\n// ✅ Good: Using Effect\nconst fetchData = Effect.gen(function* () { })\n\n// ❌ Bad: Throwing errors\nconst validate = (x: unknown) => {\n if (!x) throw new Error(\"Invalid\") // Error\n}\n\n// ✅ Good: Returning Effect with error\nconst validate = (x: unknown) =>\n x ? Effect.succeed(x) : Effect.fail(new ValidationError())\n\n// ❌ Bad: Using null/undefined directly\nconst maybeValue: string | null = null // Warn\n\n// ✅ Good: Using Option\nconst maybeValue: Option.Option = Option.none()\n```\n\n## Biome vs ESLint\n\n| Feature | Biome | ESLint |\n|---------|-------|--------|\n| Speed | ⚡ Very fast | 🐢 Slower |\n| Config | Simple JSON | Complex |\n| Plugins | Limited | Many |\n| Formatting | Built-in | Need Prettier |\n\n## Best Practices\n\n1. **Use Biome for new projects** - Faster, simpler\n2. **Enable strict rules** - Catch more bugs\n3. **Auto-fix on save** - Reduce friction\n4. **Run in CI** - Enforce standards\n5. **Ignore generated files** - Don't lint build output" - }, - { - "id": "control-flow-with-combinators", - "title": "Control Flow with Conditional Combinators", - "description": "Use conditional combinators for control flow.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst attemptAdminAction = (user: { isAdmin: boolean }) =>\n Effect.if(user.isAdmin, {\n onTrue: () => Effect.succeed(\"Admin action completed.\"),\n onFalse: () => Effect.fail(\"Permission denied.\"),\n });\n\nconst program = Effect.gen(function* () {\n // Try with admin user\n yield* Effect.logInfo(\"\\nTrying with admin user...\");\n const adminResult = yield* Effect.either(\n attemptAdminAction({ isAdmin: true })\n );\n yield* Effect.logInfo(\n `Admin result: ${adminResult._tag === \"Right\" ? adminResult.right : adminResult.left}`\n );\n\n // Try with non-admin user\n yield* Effect.logInfo(\"\\nTrying with non-admin user...\");\n const userResult = yield* Effect.either(\n attemptAdminAction({ isAdmin: false })\n );\n yield* Effect.logInfo(\n `User result: ${userResult._tag === \"Right\" ? userResult.right : userResult.left}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.if` and related combinators allow you to branch logic without leaving\nthe Effect world or breaking the flow of composition.", - "antiPattern": "Using `Effect.gen` for a single, simple conditional check can be more verbose\nthan necessary. For simple branching, `Effect.if` is often more concise.", - "explanation": "These combinators allow you to embed conditional logic directly into your\n`.pipe()` compositions, maintaining a declarative style for simple branching.", - "content": "# Control Flow with Conditional Combinators\n\n## Guideline\n\nUse declarative combinators like `Effect.if`, `Effect.when`, and\n`Effect.unless` to execute effects based on runtime conditions.\n\n## Rationale\n\nThese combinators allow you to embed conditional logic directly into your\n`.pipe()` compositions, maintaining a declarative style for simple branching.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst attemptAdminAction = (user: { isAdmin: boolean }) =>\n Effect.if(user.isAdmin, {\n onTrue: () => Effect.succeed(\"Admin action completed.\"),\n onFalse: () => Effect.fail(\"Permission denied.\"),\n });\n\nconst program = Effect.gen(function* () {\n // Try with admin user\n yield* Effect.logInfo(\"\\nTrying with admin user...\");\n const adminResult = yield* Effect.either(\n attemptAdminAction({ isAdmin: true })\n );\n yield* Effect.logInfo(\n `Admin result: ${adminResult._tag === \"Right\" ? adminResult.right : adminResult.left}`\n );\n\n // Try with non-admin user\n yield* Effect.logInfo(\"\\nTrying with non-admin user...\");\n const userResult = yield* Effect.either(\n attemptAdminAction({ isAdmin: false })\n );\n yield* Effect.logInfo(\n `User result: ${userResult._tag === \"Right\" ? userResult.right : userResult.left}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.if` and related combinators allow you to branch logic without leaving\nthe Effect world or breaking the flow of composition.\n\n## Anti-Pattern\n\nUsing `Effect.gen` for a single, simple conditional check can be more verbose\nthan necessary. For simple branching, `Effect.if` is often more concise." - }, - { - "id": "control-repetition-with-schedule", - "title": "Control Repetition with Schedule", - "description": "Use Schedule to create composable policies for controlling the repetition and retrying of effects.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "This example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\n// A simple effect that can fail\nconst flakyEffect = Effect.try({\n try: () => {\n if (Math.random() > 0.2) {\n throw new Error(\"Transient error\");\n }\n return \"Operation succeeded!\";\n },\n catch: (error: unknown) => {\n Effect.logInfo(\"Operation failed, retrying...\");\n return error;\n },\n});\n\n// --- Building a Composable Schedule ---\n\n// 1. Start with a base exponential backoff (100ms, 200ms, 400ms...)\nconst exponentialBackoff = Schedule.exponential(\"100 millis\");\n\n// 2. Add random jitter to avoid thundering herd problems\nconst withJitter = Schedule.jittered(exponentialBackoff);\n\n// 3. Limit the schedule to a maximum of 5 repetitions\nconst limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));\n\n// --- Using the Schedule ---\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting operation...\");\n const result = yield* Effect.retry(flakyEffect, limitedWithJitter);\n yield* Effect.logInfo(`Final result: ${result}`);\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "Writing manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { flakyEffect } from \"./somewhere\";\n\n// ❌ WRONG: Manual, stateful, and complex retry logic.\nfunction manualRetry(\n effect: typeof flakyEffect,\n retriesLeft: number,\n delay: number\n): Effect.Effect {\n return effect.pipe(\n Effect.catchTag(\"ApiError\", () => {\n if (retriesLeft > 0) {\n return Effect.sleep(delay).pipe(\n Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))\n );\n }\n return Effect.fail(\"ApiError\" as const);\n })\n );\n}\n\nconst program = manualRetry(flakyEffect, 5, 100);\n```", - "explanation": "While you could write manual loops or recursive functions, `Schedule` provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are:\n\n- **Declarative:** You separate the _what_ (the effect to run) from the _how_ and _when_ (the schedule it runs on).\n- **Composable:** You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs \"up to 5 times, with an exponential backoff, plus some random jitter\" by composing `Schedule.recurs`, `Schedule.exponential`, and `Schedule.jittered`.\n- **Stateful:** A `Schedule` keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.\n\n---", - "content": "## Guideline\n\nA `Schedule` is a highly-composable blueprint that defines a recurring schedule. It takes an input of type `In` (e.g., the error from a failed effect) and produces an output of type `Out` (e.g., the decision to continue). Use `Schedule` with operators like `Effect.repeat` and `Effect.retry` to control complex repeating logic.\n\n---\n\n## Rationale\n\nWhile you could write manual loops or recursive functions, `Schedule` provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are:\n\n- **Declarative:** You separate the _what_ (the effect to run) from the _how_ and _when_ (the schedule it runs on).\n- **Composable:** You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs \"up to 5 times, with an exponential backoff, plus some random jitter\" by composing `Schedule.recurs`, `Schedule.exponential`, and `Schedule.jittered`.\n- **Stateful:** A `Schedule` keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history.\n\n---\n\n## Good Example\n\nThis example demonstrates composition by creating a common, robust retry policy: exponential backoff with jitter, limited to 5 attempts.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\n// A simple effect that can fail\nconst flakyEffect = Effect.try({\n try: () => {\n if (Math.random() > 0.2) {\n throw new Error(\"Transient error\");\n }\n return \"Operation succeeded!\";\n },\n catch: (error: unknown) => {\n Effect.logInfo(\"Operation failed, retrying...\");\n return error;\n },\n});\n\n// --- Building a Composable Schedule ---\n\n// 1. Start with a base exponential backoff (100ms, 200ms, 400ms...)\nconst exponentialBackoff = Schedule.exponential(\"100 millis\");\n\n// 2. Add random jitter to avoid thundering herd problems\nconst withJitter = Schedule.jittered(exponentialBackoff);\n\n// 3. Limit the schedule to a maximum of 5 repetitions\nconst limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));\n\n// --- Using the Schedule ---\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting operation...\");\n const result = yield* Effect.retry(flakyEffect, limitedWithJitter);\n yield* Effect.logInfo(`Final result: ${result}`);\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nWriting manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { flakyEffect } from \"./somewhere\";\n\n// ❌ WRONG: Manual, stateful, and complex retry logic.\nfunction manualRetry(\n effect: typeof flakyEffect,\n retriesLeft: number,\n delay: number\n): Effect.Effect {\n return effect.pipe(\n Effect.catchTag(\"ApiError\", () => {\n if (retriesLeft > 0) {\n return Effect.sleep(delay).pipe(\n Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))\n );\n }\n return Effect.fail(\"ApiError\" as const);\n })\n );\n}\n\nconst program = manualRetry(flakyEffect, 5, 100);\n```" - }, - { - "id": "constructor-from-nullable-option-either", - "title": "Converting from Nullable, Option, or Either", - "description": "Use fromNullable, fromOption, and fromEither to lift nullable values, Option, or Either into Effects or Streams for safe, typeful interop.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Option: Convert a nullable value to an Option\nconst nullableValue: string | null = Math.random() > 0.5 ? \"hello\" : null;\nconst option = Option.fromNullable(nullableValue); // Option\n\n// Effect: Convert an Option to an Effect that may fail\nconst someValue = Option.some(42);\nconst effectFromOption = Option.match(someValue, {\n onNone: () => Effect.fail(\"No value\"),\n onSome: (value) => Effect.succeed(value),\n}); // Effect\n\n// Effect: Convert an Either to an Effect\nconst either = Either.right(\"success\");\nconst effectFromEither = Either.match(either, {\n onLeft: (error) => Effect.fail(error),\n onRight: (value) => Effect.succeed(value),\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.fromNullable` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`.\n- `Effect.fromOption` lifts an Option into an Effect, failing if the Option is `none`.\n- `Effect.fromEither` lifts an Either into an Effect, failing if the Either is `left`.", - "antiPattern": "Passing around `null`, `undefined`, or custom option/either types without converting them, which leads to unsafe, non-composable code and harder error handling.", - "explanation": "Converting to Effect, Stream, Option, or Either lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`.", - "content": "# Converting from Nullable, Option, or Either\n\n## Guideline\n\nUse the `fromNullable`, `fromOption`, and `fromEither` constructors to convert nullable values, `Option`, or `Either` into Effects or Streams. \nThis enables safe, typeful interop with legacy code, APIs, or libraries that use `null`, `undefined`, or their own option/either types.\n\n## Rationale\n\nConverting to Effect, Stream, Option, or Either lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`.\n\n## Good Example\n\n```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Option: Convert a nullable value to an Option\nconst nullableValue: string | null = Math.random() > 0.5 ? \"hello\" : null;\nconst option = Option.fromNullable(nullableValue); // Option\n\n// Effect: Convert an Option to an Effect that may fail\nconst someValue = Option.some(42);\nconst effectFromOption = Option.match(someValue, {\n onNone: () => Effect.fail(\"No value\"),\n onSome: (value) => Effect.succeed(value),\n}); // Effect\n\n// Effect: Convert an Either to an Effect\nconst either = Either.right(\"success\");\nconst effectFromEither = Either.match(either, {\n onLeft: (error) => Effect.fail(error),\n onRight: (value) => Effect.succeed(value),\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.fromNullable` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`.\n- `Effect.fromOption` lifts an Option into an Effect, failing if the Option is `none`.\n- `Effect.fromEither` lifts an Either into an Effect, failing if the Either is `left`.\n\n## Anti-Pattern\n\nPassing around `null`, `undefined`, or custom option/either types without converting them, which leads to unsafe, non-composable code and harder error handling." - }, - { - "id": "launch-http-server", - "title": "Create a Basic HTTP Server", - "description": "Use Http.server.serve with a platform-specific layer to run an HTTP application.", - "skillLevel": "beginner", - "useCases": [ - "building-apis" - ], - "example": "This example creates a minimal server that responds to all requests with \"Hello, World!\". The application logic is a simple `Effect` that returns an `Http.response`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\nimport * as http from \"http\";\n\n// Create HTTP server service\nclass HttpServer extends Effect.Service()(\"HttpServer\", {\n sync: () => ({\n start: () =>\n Effect.gen(function* () {\n const server = http.createServer(\n (req: http.IncomingMessage, res: http.ServerResponse) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello, World!\");\n }\n );\n\n // Add cleanup finalizer\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shut down\");\n })\n );\n\n // Start server with timeout\n yield* Effect.async((resume) => {\n server.on(\"error\", (error) => resume(Effect.fail(error)));\n server.listen(3456, \"localhost\", () => {\n resume(Effect.succeed(void 0));\n });\n }).pipe(\n Effect.timeout(Duration.seconds(5)),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to start server: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n );\n\n yield* Effect.logInfo(\"Server running at http://localhost:3456/\");\n\n // Run for a short duration to demonstrate the server is working\n yield* Effect.sleep(Duration.seconds(3));\n yield* Effect.logInfo(\"Server demonstration complete\");\n }),\n }),\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const server = yield* HttpServer;\n\n yield* Effect.logInfo(\"Starting HTTP server...\");\n\n yield* server.start();\n}).pipe(\n Effect.scoped // Ensure server is cleaned up properly\n);\n\n// Run the server with proper error handling\nconst programWithErrorHandling = Effect.provide(\n program,\n HttpServer.Default\n).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program failed: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling).catch(() => {\n process.exit(1);\n});\n\n/*\nTo test:\n1. Server will timeout after 5 seconds if it can't start\n2. Server runs on port 3456 to avoid conflicts\n3. Proper cleanup on shutdown\n4. Demonstrates server lifecycle: start -> run -> shutdown\n*/\n```", - "antiPattern": "The common anti-pattern is to use the raw Node.js `http` module directly, outside of the Effect runtime. This approach creates a disconnect between your application logic and the server's lifecycle.\n\n```typescript\nimport * as http from \"http\";\n\n// Manually create a server using the Node.js built-in module.\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello, World!\");\n});\n\n// Manually start the server and log the port.\nconst port = 3000;\nserver.listen(port, () => {\n console.log(`Server running at http://localhost:${port}/`);\n});\n```\n\nThis imperative approach is discouraged when building an Effect application because it forfeits all the benefits of the ecosystem. It runs outside of Effect's structured concurrency, cannot be managed by its resource-safe `Scope`, does not integrate with `Layer` for dependency injection, and requires manual error handling, making it less robust and much harder to compose with other effectful logic.", - "explanation": "In Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The `@effect/platform` package provides a platform-agnostic API for defining HTTP applications, while packages like `@effect/platform-node` provide the concrete implementation.\n\nThe core function `Http.server.serve(app)` takes your application logic and returns an `Effect` that, when run, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down.\n\nThis approach provides several key benefits:\n\n1. **Lifecycle Management**: The server's lifecycle is managed by the Effect runtime. This means structured concurrency applies, ensuring graceful shutdowns and proper resource handling automatically.\n2. **Integration**: The server is a first-class citizen in the Effect ecosystem. It can seamlessly access dependencies provided by `Layer`, use `Config` for configuration, and integrate with `Logger`.\n3. **Platform Agnosticism**: By coding to the `Http.App` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer.\n\n---", - "content": "## Guideline\n\nTo create and run a web server, define your application as an `Http.App` and execute it using `Http.server.serve`, providing a platform-specific layer like `NodeHttpServer.layer`.\n\n---\n\n## Rationale\n\nIn Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The `@effect/platform` package provides a platform-agnostic API for defining HTTP applications, while packages like `@effect/platform-node` provide the concrete implementation.\n\nThe core function `Http.server.serve(app)` takes your application logic and returns an `Effect` that, when run, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down.\n\nThis approach provides several key benefits:\n\n1. **Lifecycle Management**: The server's lifecycle is managed by the Effect runtime. This means structured concurrency applies, ensuring graceful shutdowns and proper resource handling automatically.\n2. **Integration**: The server is a first-class citizen in the Effect ecosystem. It can seamlessly access dependencies provided by `Layer`, use `Config` for configuration, and integrate with `Logger`.\n3. **Platform Agnosticism**: By coding to the `Http.App` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer.\n\n---\n\n## Good Example\n\nThis example creates a minimal server that responds to all requests with \"Hello, World!\". The application logic is a simple `Effect` that returns an `Http.response`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\nimport * as http from \"http\";\n\n// Create HTTP server service\nclass HttpServer extends Effect.Service()(\"HttpServer\", {\n sync: () => ({\n start: () =>\n Effect.gen(function* () {\n const server = http.createServer(\n (req: http.IncomingMessage, res: http.ServerResponse) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello, World!\");\n }\n );\n\n // Add cleanup finalizer\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shut down\");\n })\n );\n\n // Start server with timeout\n yield* Effect.async((resume) => {\n server.on(\"error\", (error) => resume(Effect.fail(error)));\n server.listen(3456, \"localhost\", () => {\n resume(Effect.succeed(void 0));\n });\n }).pipe(\n Effect.timeout(Duration.seconds(5)),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to start server: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n );\n\n yield* Effect.logInfo(\"Server running at http://localhost:3456/\");\n\n // Run for a short duration to demonstrate the server is working\n yield* Effect.sleep(Duration.seconds(3));\n yield* Effect.logInfo(\"Server demonstration complete\");\n }),\n }),\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const server = yield* HttpServer;\n\n yield* Effect.logInfo(\"Starting HTTP server...\");\n\n yield* server.start();\n}).pipe(\n Effect.scoped // Ensure server is cleaned up properly\n);\n\n// Run the server with proper error handling\nconst programWithErrorHandling = Effect.provide(\n program,\n HttpServer.Default\n).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program failed: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling).catch(() => {\n process.exit(1);\n});\n\n/*\nTo test:\n1. Server will timeout after 5 seconds if it can't start\n2. Server runs on port 3456 to avoid conflicts\n3. Proper cleanup on shutdown\n4. Demonstrates server lifecycle: start -> run -> shutdown\n*/\n```\n\n## Anti-Pattern\n\nThe common anti-pattern is to use the raw Node.js `http` module directly, outside of the Effect runtime. This approach creates a disconnect between your application logic and the server's lifecycle.\n\n```typescript\nimport * as http from \"http\";\n\n// Manually create a server using the Node.js built-in module.\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello, World!\");\n});\n\n// Manually start the server and log the port.\nconst port = 3000;\nserver.listen(port, () => {\n console.log(`Server running at http://localhost:${port}/`);\n});\n```\n\nThis imperative approach is discouraged when building an Effect application because it forfeits all the benefits of the ecosystem. It runs outside of Effect's structured concurrency, cannot be managed by its resource-safe `Scope`, does not integrate with `Layer` for dependency injection, and requires manual error handling, making it less robust and much harder to compose with other effectful logic." - }, - { - "id": "create-managed-runtime-for-scoped-resources", - "title": "Create a Managed Runtime for Scoped Resources", - "description": "Create a managed runtime for scoped resources.", - "skillLevel": "advanced", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Layer } from \"effect\";\n\nclass DatabasePool extends Effect.Service()(\"DbPool\", {\n effect: Effect.gen(function* () {\n yield* Effect.log(\"Acquiring pool\");\n return {\n query: () => Effect.succeed(\"result\"),\n };\n }),\n}) {}\n\n// Create a program that uses the DatabasePool service\nconst program = Effect.gen(function* () {\n const db = yield* DatabasePool;\n yield* Effect.log(\"Using DB\");\n yield* db.query();\n});\n\n// Run the program with the service implementation\nEffect.runPromise(\n program.pipe(Effect.provide(DatabasePool.Default), Effect.scoped)\n);\n```\n\n**Explanation:** \n`Layer.launch` ensures that resources are acquired and released safely, even\nin the event of errors or interruptions.", - "antiPattern": "Do not use `Layer.toRuntime` with layers that contain scoped resources. This\nwill acquire the resource, but the runtime has no mechanism to release it,\nleading to resource leaks.", - "explanation": "`Layer.launch` is designed for resource safety. It acquires all resources,\nprovides them to your effect, and—crucially—guarantees that all registered\nfinalizers are executed upon completion or interruption.", - "content": "# Create a Managed Runtime for Scoped Resources\n\n## Guideline\n\nFor services that manage resources needing explicit cleanup (e.g., a database\nconnection), define them in a `Layer` using `Layer.scoped`. Then, use\n`Layer.launch` to provide this layer to your application.\n\n## Rationale\n\n`Layer.launch` is designed for resource safety. It acquires all resources,\nprovides them to your effect, and—crucially—guarantees that all registered\nfinalizers are executed upon completion or interruption.\n\n## Good Example\n\n```typescript\nimport { Effect, Layer } from \"effect\";\n\nclass DatabasePool extends Effect.Service()(\"DbPool\", {\n effect: Effect.gen(function* () {\n yield* Effect.log(\"Acquiring pool\");\n return {\n query: () => Effect.succeed(\"result\"),\n };\n }),\n}) {}\n\n// Create a program that uses the DatabasePool service\nconst program = Effect.gen(function* () {\n const db = yield* DatabasePool;\n yield* Effect.log(\"Using DB\");\n yield* db.query();\n});\n\n// Run the program with the service implementation\nEffect.runPromise(\n program.pipe(Effect.provide(DatabasePool.Default), Effect.scoped)\n);\n```\n\n**Explanation:** \n`Layer.launch` ensures that resources are acquired and released safely, even\nin the event of errors or interruptions.\n\n## Anti-Pattern\n\nDo not use `Layer.toRuntime` with layers that contain scoped resources. This\nwill acquire the resource, but the runtime has no mechanism to release it,\nleading to resource leaks." - }, - { - "id": "create-reusable-runtime-from-layers", - "title": "Create a Reusable Runtime from Layers", - "description": "Create a reusable runtime from layers.", - "skillLevel": "advanced", - "useCases": [ - "project-setup--execution" - ], - "example": "```typescript\nimport { Effect, Layer, Runtime } from \"effect\";\n\nclass GreeterService extends Effect.Service()(\"Greeter\", {\n sync: () => ({\n greet: (name: string) => Effect.sync(() => `Hello ${name}`),\n }),\n}) {}\n\nconst runtime = Effect.runSync(\n Layer.toRuntime(GreeterService.Default).pipe(Effect.scoped)\n);\n\n// In a server, you would reuse `run` for every request.\nRuntime.runPromise(runtime)(Effect.log(\"Hello\"));\n```\n\n**Explanation:** \nBy compiling your layers into a Runtime once, you avoid rebuilding the\ndependency graph for every effect execution.", - "antiPattern": "For a long-running application, avoid providing layers and running an effect\nin a single operation. This forces Effect to rebuild the dependency graph on\nevery execution.", - "explanation": "Building the dependency graph from layers has a one-time cost. Creating a\n`Runtime` once when your application starts is highly efficient for\nlong-running applications.", - "content": "# Create a Reusable Runtime from Layers\n\n## Guideline\n\nFor applications that need to run multiple effects (e.g., a web server), use\n`Layer.toRuntime(appLayer)` to compile your dependency graph into a single,\nreusable `Runtime` object.\n\n## Rationale\n\nBuilding the dependency graph from layers has a one-time cost. Creating a\n`Runtime` once when your application starts is highly efficient for\nlong-running applications.\n\n## Good Example\n\n```typescript\nimport { Effect, Layer, Runtime } from \"effect\";\n\nclass GreeterService extends Effect.Service()(\"Greeter\", {\n sync: () => ({\n greet: (name: string) => Effect.sync(() => `Hello ${name}`),\n }),\n}) {}\n\nconst runtime = Effect.runSync(\n Layer.toRuntime(GreeterService.Default).pipe(Effect.scoped)\n);\n\n// In a server, you would reuse `run` for every request.\nRuntime.runPromise(runtime)(Effect.log(\"Hello\"));\n```\n\n**Explanation:** \nBy compiling your layers into a Runtime once, you avoid rebuilding the\ndependency graph for every effect execution.\n\n## Anti-Pattern\n\nFor a long-running application, avoid providing layers and running an effect\nin a single operation. This forces Effect to rebuild the dependency graph on\nevery execution." - }, - { - "id": "scoped-service-layer", - "title": "Create a Service Layer from a Managed Resource", - "description": "Provide a managed resource to the application context using `Layer.scoped`.", - "skillLevel": "intermediate", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Console } from \"effect\";\n\n// 1. Define the service interface\ninterface DatabaseService {\n readonly query: (sql: string) => Effect.Effect;\n}\n\n// 2. Define the service implementation with scoped resource management\nclass Database extends Effect.Service()(\"Database\", {\n // The scoped property manages the resource lifecycle\n scoped: Effect.gen(function* () {\n const id = Math.floor(Math.random() * 1000);\n\n // Acquire the connection\n yield* Effect.log(`[Pool ${id}] Acquired`);\n\n // Setup cleanup to run when scope closes\n yield* Effect.addFinalizer(() => Effect.log(`[Pool ${id}] Released`));\n\n // Return the service implementation\n return {\n query: (sql: string) =>\n Effect.sync(() => [`Result for '${sql}' from pool ${id}`]),\n };\n }),\n}) {}\n\n// 3. Use the service in your program\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n const users = yield* db.query(\"SELECT * FROM users\");\n yield* Effect.log(`Query successful: ${users[0]}`);\n});\n\n// 4. Run the program with scoped resource management\nEffect.runPromise(\n Effect.scoped(program).pipe(Effect.provide(Database.Default))\n);\n\n/*\nOutput:\n[Pool 458] Acquired\nQuery successful: Result for 'SELECT * FROM users' from pool 458\n[Pool 458] Released\n*/\n```\n\n**Explanation:**\nThe `Effect.Service` helper creates the `Database` class, which acts as both the service definition and its context key (Tag). The `Database.Live` layer connects this service to a concrete, lifecycle-managed implementation. When `program` asks for the `Database` service, the Effect runtime uses the `Live` layer to run the `acquire` effect once, caches the resulting `DbPool`, and injects it. The `release` effect is automatically run when the program completes.", - "antiPattern": "Creating and exporting a global singleton instance of a resource. This tightly couples your application to a specific implementation, makes testing difficult, and offers no guarantees about graceful shutdown.\n\n```typescript\n// ANTI-PATTERN: Global singleton\nexport const dbPool = makeDbPoolSync(); // Eagerly created, hard to test/mock\n\nfunction someBusinessLogic() {\n // This function has a hidden dependency on the global dbPool\n return dbPool.query(\"SELECT * FROM products\");\n}\n```", - "explanation": "This pattern is the key to building robust, testable, and leak-proof applications in Effect. It elevates a managed resource into a first-class service that can be used anywhere in your application. The `Effect.Service` helper simplifies defining the service's interface and context key. This approach decouples your business logic from the concrete implementation, as the logic only depends on the abstract service. The `Layer` declaratively handles the resource's entire lifecycle, ensuring it is acquired lazily, shared safely, and released automatically.", - "content": "# Create a Service Layer from a Managed Resource\n\n## Guideline\n\nDefine a service using `class MyService extends Effect.Service(...)`. Implement the service using the `scoped` property of the service class. This property should be a scoped `Effect` (typically from `Effect.acquireRelease`) that builds and releases the underlying resource.\n\n## Rationale\n\nThis pattern is the key to building robust, testable, and leak-proof applications in Effect. It elevates a managed resource into a first-class service that can be used anywhere in your application. The `Effect.Service` helper simplifies defining the service's interface and context key. This approach decouples your business logic from the concrete implementation, as the logic only depends on the abstract service. The `Layer` declaratively handles the resource's entire lifecycle, ensuring it is acquired lazily, shared safely, and released automatically.\n\n## Good Example\n\n```typescript\nimport { Effect, Console } from \"effect\";\n\n// 1. Define the service interface\ninterface DatabaseService {\n readonly query: (sql: string) => Effect.Effect;\n}\n\n// 2. Define the service implementation with scoped resource management\nclass Database extends Effect.Service()(\"Database\", {\n // The scoped property manages the resource lifecycle\n scoped: Effect.gen(function* () {\n const id = Math.floor(Math.random() * 1000);\n\n // Acquire the connection\n yield* Effect.log(`[Pool ${id}] Acquired`);\n\n // Setup cleanup to run when scope closes\n yield* Effect.addFinalizer(() => Effect.log(`[Pool ${id}] Released`));\n\n // Return the service implementation\n return {\n query: (sql: string) =>\n Effect.sync(() => [`Result for '${sql}' from pool ${id}`]),\n };\n }),\n}) {}\n\n// 3. Use the service in your program\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n const users = yield* db.query(\"SELECT * FROM users\");\n yield* Effect.log(`Query successful: ${users[0]}`);\n});\n\n// 4. Run the program with scoped resource management\nEffect.runPromise(\n Effect.scoped(program).pipe(Effect.provide(Database.Default))\n);\n\n/*\nOutput:\n[Pool 458] Acquired\nQuery successful: Result for 'SELECT * FROM users' from pool 458\n[Pool 458] Released\n*/\n```\n\n**Explanation:**\nThe `Effect.Service` helper creates the `Database` class, which acts as both the service definition and its context key (Tag). The `Database.Live` layer connects this service to a concrete, lifecycle-managed implementation. When `program` asks for the `Database` service, the Effect runtime uses the `Live` layer to run the `acquire` effect once, caches the resulting `DbPool`, and injects it. The `release` effect is automatically run when the program completes.\n\n## Anti-Pattern\n\nCreating and exporting a global singleton instance of a resource. This tightly couples your application to a specific implementation, makes testing difficult, and offers no guarantees about graceful shutdown.\n\n```typescript\n// ANTI-PATTERN: Global singleton\nexport const dbPool = makeDbPoolSync(); // Eagerly created, hard to test/mock\n\nfunction someBusinessLogic() {\n // This function has a hidden dependency on the global dbPool\n return dbPool.query(\"SELECT * FROM products\");\n}\n```" - }, - { - "id": "stream-from-iterable", - "title": "Create a Stream from a List", - "description": "Use Stream.fromIterable to begin a pipeline from an in-memory collection.", - "skillLevel": "beginner", - "useCases": [ - "building-data-pipelines" - ], - "example": "This example takes a simple array of numbers, creates a stream from it, performs a transformation on each number, and then runs the stream to collect the results.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\nconst numbers = [1, 2, 3, 4, 5];\n\n// Create a stream from the array of numbers.\nconst program = Stream.fromIterable(numbers).pipe(\n // Perform a simple, synchronous transformation on each item.\n Stream.map((n) => `Item: ${n}`),\n // Run the stream and collect all the transformed items into a Chunk.\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const processedItems = yield* program;\n yield* Effect.log(\n `Processed items: ${JSON.stringify(Chunk.toArray(processedItems))}`\n );\n return processedItems;\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n[ 'Item: 1', 'Item: 2', 'Item: 3', 'Item: 4', 'Item: 5' ]\n*/\n```", - "antiPattern": "The common alternative is to use standard array methods like `.map()` or a `for...of` loop. While perfectly fine for simple, synchronous tasks, this approach is an anti-pattern when building a _pipeline_.\n\n```typescript\nconst numbers = [1, 2, 3, 4, 5];\n\n// Using Array.prototype.map\nconst processedItems = numbers.map((n) => `Item: ${n}`);\n\nconsole.log(processedItems);\n```\n\nThis is an anti-pattern in the context of building a larger pipeline because:\n\n1. **It's Not Composable with Effects**: The result is just a new array. If the next step in your pipeline was an asynchronous database call for each item, you couldn't simply `.pipe()` the result into it. You would have to leave the synchronous world of `.map()` and start a new `Effect.forEach`, breaking the unified pipeline structure.\n2. **It's Eager**: The `.map()` operation processes the entire array at once. `Stream` is lazy; it only processes items as they are requested by downstream consumers, which is far more efficient for large collections or complex transformations.", - "explanation": "Every data pipeline needs a source. The simplest and most common source is a pre-existing list of items in memory. `Stream.fromIterable` is the bridge from standard JavaScript data structures to the powerful, composable world of Effect's `Stream`.\n\nThis pattern is fundamental for several reasons:\n\n1. **Entry Point**: It's the \"Hello, World!\" of data pipelines, providing the easiest way to start experimenting with stream transformations.\n2. **Testing**: In tests, you frequently need to simulate a data source (like a database query or API call). Creating a stream from a mock array of data is the standard way to do this, allowing you to test your pipeline's logic in isolation.\n3. **Composability**: It transforms a static, eager data structure (an array) into a lazy, pull-based stream. This allows you to pipe it into the rest of the Effect ecosystem, enabling asynchronous operations, concurrency, and resource management in subsequent steps.\n\n---", - "content": "## Guideline\n\nTo start a data pipeline from an existing in-memory collection like an array, use `Stream.fromIterable`.\n\n---\n\n## Rationale\n\nEvery data pipeline needs a source. The simplest and most common source is a pre-existing list of items in memory. `Stream.fromIterable` is the bridge from standard JavaScript data structures to the powerful, composable world of Effect's `Stream`.\n\nThis pattern is fundamental for several reasons:\n\n1. **Entry Point**: It's the \"Hello, World!\" of data pipelines, providing the easiest way to start experimenting with stream transformations.\n2. **Testing**: In tests, you frequently need to simulate a data source (like a database query or API call). Creating a stream from a mock array of data is the standard way to do this, allowing you to test your pipeline's logic in isolation.\n3. **Composability**: It transforms a static, eager data structure (an array) into a lazy, pull-based stream. This allows you to pipe it into the rest of the Effect ecosystem, enabling asynchronous operations, concurrency, and resource management in subsequent steps.\n\n---\n\n## Good Example\n\nThis example takes a simple array of numbers, creates a stream from it, performs a transformation on each number, and then runs the stream to collect the results.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\nconst numbers = [1, 2, 3, 4, 5];\n\n// Create a stream from the array of numbers.\nconst program = Stream.fromIterable(numbers).pipe(\n // Perform a simple, synchronous transformation on each item.\n Stream.map((n) => `Item: ${n}`),\n // Run the stream and collect all the transformed items into a Chunk.\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const processedItems = yield* program;\n yield* Effect.log(\n `Processed items: ${JSON.stringify(Chunk.toArray(processedItems))}`\n );\n return processedItems;\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n[ 'Item: 1', 'Item: 2', 'Item: 3', 'Item: 4', 'Item: 5' ]\n*/\n```\n\n## Anti-Pattern\n\nThe common alternative is to use standard array methods like `.map()` or a `for...of` loop. While perfectly fine for simple, synchronous tasks, this approach is an anti-pattern when building a _pipeline_.\n\n```typescript\nconst numbers = [1, 2, 3, 4, 5];\n\n// Using Array.prototype.map\nconst processedItems = numbers.map((n) => `Item: ${n}`);\n\nconsole.log(processedItems);\n```\n\nThis is an anti-pattern in the context of building a larger pipeline because:\n\n1. **It's Not Composable with Effects**: The result is just a new array. If the next step in your pipeline was an asynchronous database call for each item, you couldn't simply `.pipe()` the result into it. You would have to leave the synchronous world of `.map()` and start a new `Effect.forEach`, breaking the unified pipeline structure.\n2. **It's Eager**: The `.map()` operation processes the entire array at once. `Stream` is lazy; it only processes items as they are requested by downstream consumers, which is far more efficient for large collections or complex transformations." - }, - { - "id": "create-a-testable-http-client-service", - "title": "Create a Testable HTTP Client Service", - "description": "Define an HttpClient service with distinct Live and Test layers to enable testable API interactions.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "### 1. Define the Service\n\n```typescript\nimport { Effect, Data, Layer } from \"effect\";\n\ninterface HttpErrorType {\n readonly _tag: \"HttpError\";\n readonly error: unknown;\n}\n\nconst HttpError = Data.tagged(\"HttpError\");\n\ninterface HttpClientType {\n readonly get: (url: string) => Effect.Effect;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n get: (url: string): Effect.Effect =>\n Effect.tryPromise(() =>\n fetch(url).then((res) => res.json() as T)\n ).pipe(Effect.catchAll((error) => Effect.fail(HttpError({ error })))),\n }),\n}) {}\n\n// Test implementation\nconst TestLayer = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (_url: string) => Effect.succeed({ title: \"Mock Data\" } as T),\n })\n);\n\n// Example usage\nconst program = Effect.gen(function* () {\n const client = yield* HttpClient;\n yield* Effect.logInfo(\"Fetching data...\");\n const data = yield* client.get<{ title: string }>(\n \"https://api.example.com/data\"\n );\n yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);\n});\n\n// Run with test implementation\nEffect.runPromise(Effect.provide(program, TestLayer));\n```\n\n### 2. Create the Live Implementation\n\n```typescript\nimport { Effect, Data, Layer } from \"effect\";\n\ninterface HttpErrorType {\n readonly _tag: \"HttpError\";\n readonly error: unknown;\n}\n\nconst HttpError = Data.tagged(\"HttpError\");\n\ninterface HttpClientType {\n readonly get: (url: string) => Effect.Effect;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n get: (url: string): Effect.Effect =>\n Effect.tryPromise({\n try: () => fetch(url).then((res) => res.json()),\n catch: (error) => HttpError({ error }),\n }),\n }),\n}) {}\n\n// Test implementation\nconst TestLayer = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (_url: string) => Effect.succeed({ title: \"Mock Data\" } as T),\n })\n);\n\n// Example usage\nconst program = Effect.gen(function* () {\n const client = yield* HttpClient;\n yield* Effect.logInfo(\"Fetching data...\");\n const data = yield* client.get<{ title: string }>(\n \"https://api.example.com/data\"\n );\n yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);\n});\n\n// Run with test implementation\nEffect.runPromise(Effect.provide(program, TestLayer));\n```\n\n### 3. Create the Test Implementation\n\n```typescript\n// src/services/HttpClientTest.ts\nimport { Effect, Layer } from \"effect\";\nimport { HttpClient } from \"./HttpClient\";\n\nexport const HttpClientTest = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (url) => Effect.succeed({ mock: \"data\", url }),\n })\n);\n```\n\n### 4. Usage in Business Logic\n\nYour business logic is now clean and only depends on the abstract `HttpClient`.\n\n```typescript\n// src/features/User/UserService.ts\nimport { Effect } from \"effect\";\nimport { HttpClient } from \"../../services/HttpClient\";\n\nexport const getUserFromApi = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient;\n const data = yield* client.get(`https://api.example.com/users/${id}`);\n // ... logic to parse and return user\n return data;\n });\n```\n\n---", - "antiPattern": "Calling `fetch` directly from within your business logic functions. This creates a hard dependency on the global `fetch` API, making the function difficult to test and reuse.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This function is not easily testable.\nexport const getUserDirectly = (id: number) =>\n Effect.tryPromise({\n try: () =>\n fetch(`https://api.example.com/users/${id}`).then((res) => res.json()),\n catch: () => \"ApiError\" as const,\n });\n```", - "explanation": "Directly using `fetch` in your business logic makes it nearly impossible to test. Your tests would become slow, flaky (dependent on network conditions), and could have unintended side effects.\n\nBy abstracting the HTTP client into a service, you decouple your application's logic from the specific implementation of how HTTP requests are made. Your business logic depends only on the abstract `HttpClient` interface. In production, you provide the `Live` layer. In tests, you provide the `Test` layer. This makes your tests fast, deterministic, and reliable.\n\n---", - "content": "## Guideline\n\nTo interact with external APIs, define an `HttpClient` service. Create two separate `Layer` implementations for this service:\n\n1. **`HttpClientLive`**: The production implementation that uses a real HTTP client (like `fetch`) to make network requests.\n2. **`HttpClientTest`**: A test implementation that returns mock data, allowing you to test your business logic without making actual network calls.\n\n---\n\n## Rationale\n\nDirectly using `fetch` in your business logic makes it nearly impossible to test. Your tests would become slow, flaky (dependent on network conditions), and could have unintended side effects.\n\nBy abstracting the HTTP client into a service, you decouple your application's logic from the specific implementation of how HTTP requests are made. Your business logic depends only on the abstract `HttpClient` interface. In production, you provide the `Live` layer. In tests, you provide the `Test` layer. This makes your tests fast, deterministic, and reliable.\n\n---\n\n## Good Example\n\n### 1. Define the Service\n\n```typescript\nimport { Effect, Data, Layer } from \"effect\";\n\ninterface HttpErrorType {\n readonly _tag: \"HttpError\";\n readonly error: unknown;\n}\n\nconst HttpError = Data.tagged(\"HttpError\");\n\ninterface HttpClientType {\n readonly get: (url: string) => Effect.Effect;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n get: (url: string): Effect.Effect =>\n Effect.tryPromise(() =>\n fetch(url).then((res) => res.json() as T)\n ).pipe(Effect.catchAll((error) => Effect.fail(HttpError({ error })))),\n }),\n}) {}\n\n// Test implementation\nconst TestLayer = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (_url: string) => Effect.succeed({ title: \"Mock Data\" } as T),\n })\n);\n\n// Example usage\nconst program = Effect.gen(function* () {\n const client = yield* HttpClient;\n yield* Effect.logInfo(\"Fetching data...\");\n const data = yield* client.get<{ title: string }>(\n \"https://api.example.com/data\"\n );\n yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);\n});\n\n// Run with test implementation\nEffect.runPromise(Effect.provide(program, TestLayer));\n```\n\n### 2. Create the Live Implementation\n\n```typescript\nimport { Effect, Data, Layer } from \"effect\";\n\ninterface HttpErrorType {\n readonly _tag: \"HttpError\";\n readonly error: unknown;\n}\n\nconst HttpError = Data.tagged(\"HttpError\");\n\ninterface HttpClientType {\n readonly get: (url: string) => Effect.Effect;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n get: (url: string): Effect.Effect =>\n Effect.tryPromise({\n try: () => fetch(url).then((res) => res.json()),\n catch: (error) => HttpError({ error }),\n }),\n }),\n}) {}\n\n// Test implementation\nconst TestLayer = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (_url: string) => Effect.succeed({ title: \"Mock Data\" } as T),\n })\n);\n\n// Example usage\nconst program = Effect.gen(function* () {\n const client = yield* HttpClient;\n yield* Effect.logInfo(\"Fetching data...\");\n const data = yield* client.get<{ title: string }>(\n \"https://api.example.com/data\"\n );\n yield* Effect.logInfo(`Received data: ${JSON.stringify(data)}`);\n});\n\n// Run with test implementation\nEffect.runPromise(Effect.provide(program, TestLayer));\n```\n\n### 3. Create the Test Implementation\n\n```typescript\n// src/services/HttpClientTest.ts\nimport { Effect, Layer } from \"effect\";\nimport { HttpClient } from \"./HttpClient\";\n\nexport const HttpClientTest = Layer.succeed(\n HttpClient,\n HttpClient.of({\n get: (url) => Effect.succeed({ mock: \"data\", url }),\n })\n);\n```\n\n### 4. Usage in Business Logic\n\nYour business logic is now clean and only depends on the abstract `HttpClient`.\n\n```typescript\n// src/features/User/UserService.ts\nimport { Effect } from \"effect\";\nimport { HttpClient } from \"../../services/HttpClient\";\n\nexport const getUserFromApi = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient;\n const data = yield* client.get(`https://api.example.com/users/${id}`);\n // ... logic to parse and return user\n return data;\n });\n```\n\n---\n\n## Anti-Pattern\n\nCalling `fetch` directly from within your business logic functions. This creates a hard dependency on the global `fetch` API, making the function difficult to test and reuse.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This function is not easily testable.\nexport const getUserDirectly = (id: number) =>\n Effect.tryPromise({\n try: () =>\n fetch(`https://api.example.com/users/${id}`).then((res) => res.json()),\n catch: () => \"ApiError\" as const,\n });\n```" - }, - { - "id": "observability-dashboards", - "title": "Create Observability Dashboards", - "description": "Create focused dashboards that answer specific questions about system health.", - "skillLevel": "advanced", - "useCases": [ - "observability" - ], - "example": "", - "antiPattern": "", - "explanation": "Good dashboards provide:\n\n1. **Quick health check** - See problems at a glance\n2. **Trend analysis** - Spot gradual degradation\n3. **Debugging aid** - Correlate metrics during incidents\n4. **Capacity planning** - Forecast resource needs\n\n---", - "content": "## Guideline\n\nDesign dashboards that answer specific questions about system health, performance, and user experience.\n\n---\n\n## Rationale\n\nGood dashboards provide:\n\n1. **Quick health check** - See problems at a glance\n2. **Trend analysis** - Spot gradual degradation\n3. **Debugging aid** - Correlate metrics during incidents\n4. **Capacity planning** - Forecast resource needs\n\n---\n\n## Dashboard Patterns\n\n### 1. Service Overview Dashboard\n\n```typescript\nimport { Effect, Metric } from \"effect\"\n\n// ============================================\n// Key metrics for overview dashboard\n// ============================================\n\n// RED metrics (Rate, Errors, Duration)\nconst requestRate = Metric.counter(\"http_requests_total\")\nconst errorRate = Metric.counter(\"http_errors_total\")\nconst requestDuration = Metric.histogram(\"http_request_duration_seconds\", {\n boundaries: [0.01, 0.05, 0.1, 0.5, 1, 5],\n})\n\n// USE metrics (Utilization, Saturation, Errors)\nconst cpuUtilization = Metric.gauge(\"cpu_utilization_percent\")\nconst memoryUsage = Metric.gauge(\"memory_usage_bytes\")\nconst connectionPoolSize = Metric.gauge(\"connection_pool_active\")\n\n// Business metrics\nconst ordersProcessed = Metric.counter(\"orders_processed_total\")\nconst revenueTotal = Metric.counter(\"revenue_dollars_total\")\n```\n\n### 2. Grafana Dashboard JSON\n\n```json\n{\n \"title\": \"Effect Application Overview\",\n \"panels\": [\n {\n \"title\": \"Request Rate\",\n \"type\": \"timeseries\",\n \"targets\": [\n {\n \"expr\": \"rate(http_requests_total[5m])\",\n \"legendFormat\": \"{{method}} {{path}}\"\n }\n ],\n \"gridPos\": { \"x\": 0, \"y\": 0, \"w\": 8, \"h\": 6 }\n },\n {\n \"title\": \"Error Rate\",\n \"type\": \"timeseries\",\n \"targets\": [\n {\n \"expr\": \"rate(http_errors_total[5m]) / rate(http_requests_total[5m]) * 100\",\n \"legendFormat\": \"Error %\"\n }\n ],\n \"gridPos\": { \"x\": 8, \"y\": 0, \"w\": 8, \"h\": 6 }\n },\n {\n \"title\": \"P99 Latency\",\n \"type\": \"timeseries\",\n \"targets\": [\n {\n \"expr\": \"histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))\",\n \"legendFormat\": \"P99\"\n }\n ],\n \"gridPos\": { \"x\": 16, \"y\": 0, \"w\": 8, \"h\": 6 }\n },\n {\n \"title\": \"Active Connections\",\n \"type\": \"gauge\",\n \"targets\": [\n {\n \"expr\": \"active_connections\",\n \"legendFormat\": \"Connections\"\n }\n ],\n \"gridPos\": { \"x\": 0, \"y\": 6, \"w\": 6, \"h\": 4 }\n }\n ]\n}\n```\n\n### 3. SLO Dashboard\n\n```typescript\n// ============================================\n// SLO-focused metrics\n// ============================================\n\n// Availability: % of successful requests\nconst availabilitySLO = `\n sum(rate(http_requests_total{status!~\"5..\"}[5m]))\n /\n sum(rate(http_requests_total[5m]))\n * 100\n`\n\n// Latency: % of requests under threshold\nconst latencySLO = `\n sum(rate(http_request_duration_seconds_bucket{le=\"0.5\"}[5m]))\n /\n sum(rate(http_request_duration_seconds_count[5m]))\n * 100\n`\n\n// Error budget remaining\nconst errorBudget = `\n 1 - (\n (1 - (sum(rate(http_requests_total{status!~\"5..\"}[30d])) / sum(rate(http_requests_total[30d]))))\n /\n (1 - 0.999) # 99.9% SLO target\n )\n`\n```\n\n### 4. Effect-Specific Dashboard\n\n```typescript\nimport { Effect, Metric } from \"effect\"\n\n// Effect runtime metrics\nconst fiberCount = Metric.gauge(\"effect_fibers_active\")\nconst fiberCreated = Metric.counter(\"effect_fibers_created_total\")\nconst effectDuration = Metric.histogram(\"effect_duration_seconds\")\n\n// Service layer metrics\nconst serviceCallsTotal = Metric.counter(\"service_calls_total\")\nconst serviceErrors = Metric.counter(\"service_errors_total\")\n\n// Instrument Effect programs\nconst instrumentedProgram = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n yield* Metric.increment(serviceCallsTotal.pipe(Metric.tagged(\"service\", name)))\n const startTime = Date.now()\n\n const result = yield* effect.pipe(\n Effect.tapError(() =>\n Metric.increment(serviceErrors.pipe(Metric.tagged(\"service\", name)))\n )\n )\n\n const duration = (Date.now() - startTime) / 1000\n yield* Metric.update(\n effectDuration.pipe(Metric.tagged(\"service\", name)),\n duration\n )\n\n return result\n })\n```\n\n## Dashboard Layout\n\n```\n┌─────────────────────────────────────────────────────────┐\n│ Service Overview │\n├──────────────────┬──────────────────┬──────────────────┤\n│ Request Rate │ Error Rate │ P99 Latency │\n│ ▄▄▄▄▄▄▄▄▄▄▄ │ ▄▄▄▄▄▄▄▄▄▄▄ │ ▄▄▄▄▄▄▄▄▄▄▄ │\n├──────────────────┴──────────────────┴──────────────────┤\n│ Resource Usage │\n├──────────────────┬──────────────────┬──────────────────┤\n│ CPU: 45% │ Memory: 2.1GB │ Connections: 42│\n├──────────────────┴──────────────────┴──────────────────┤\n│ SLO Compliance │\n├──────────────────┬──────────────────┬──────────────────┤\n│ Availability │ Latency SLO │ Error Budget │\n│ 99.95% │ 98.2% │ 75% remaining │\n└──────────────────┴──────────────────┴──────────────────┘\n```\n\n## Key Queries\n\n| Metric | PromQL |\n|--------|--------|\n| Request rate | `rate(http_requests_total[5m])` |\n| Error rate | `rate(http_errors_total[5m]) / rate(http_requests_total[5m])` |\n| P50 latency | `histogram_quantile(0.5, rate(duration_bucket[5m]))` |\n| P99 latency | `histogram_quantile(0.99, rate(duration_bucket[5m]))` |\n| Saturation | `active / max` |\n\n## Best Practices\n\n1. **Start with RED** - Rate, Errors, Duration\n2. **Add USE** - Utilization, Saturation, Errors\n3. **Include SLOs** - Show compliance\n4. **Group logically** - Related metrics together\n5. **Use consistent time ranges** - 5m, 1h, 24h" - }, - { - "id": "create-pre-resolved-effect", - "title": "Create Pre-resolved Effects with succeed and fail", - "description": "Create pre-resolved effects with succeed and fail.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Data } from \"effect\";\n\n// Create a custom error type\nclass MyError extends Data.TaggedError(\"MyError\") {}\n\n// Create a program that demonstrates pre-resolved effects\nconst program = Effect.gen(function* () {\n // Success effect\n yield* Effect.logInfo(\"Running success effect...\");\n yield* Effect.gen(function* () {\n const value = yield* Effect.succeed(42);\n yield* Effect.logInfo(`Success value: ${value}`);\n });\n\n // Failure effect\n yield* Effect.logInfo(\"\\nRunning failure effect...\");\n yield* Effect.gen(function* () {\n // Use return yield* for effects that never succeed\n return yield* Effect.fail(new MyError());\n }).pipe(\n Effect.catchTag(\"MyError\", (error) =>\n Effect.logInfo(`Error occurred: ${error._tag}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `Effect.succeed` for values you already have, and `Effect.fail` for\nimmediate, known errors.", - "antiPattern": "Do not wrap a static value in `Effect.sync`. While it works, `Effect.succeed`\nis more descriptive and direct for values that are already available.", - "explanation": "These are the simplest effect constructors, essential for returning static\nvalues within functions that must return an `Effect`.", - "content": "# Create Pre-resolved Effects with succeed and fail\n\n## Guideline\n\nTo lift a pure, already-known value into an `Effect`, use `Effect.succeed()`.\nTo represent an immediate and known failure, use `Effect.fail()`.\n\n## Rationale\n\nThese are the simplest effect constructors, essential for returning static\nvalues within functions that must return an `Effect`.\n\n## Good Example\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Create a custom error type\nclass MyError extends Data.TaggedError(\"MyError\") {}\n\n// Create a program that demonstrates pre-resolved effects\nconst program = Effect.gen(function* () {\n // Success effect\n yield* Effect.logInfo(\"Running success effect...\");\n yield* Effect.gen(function* () {\n const value = yield* Effect.succeed(42);\n yield* Effect.logInfo(`Success value: ${value}`);\n });\n\n // Failure effect\n yield* Effect.logInfo(\"\\nRunning failure effect...\");\n yield* Effect.gen(function* () {\n // Use return yield* for effects that never succeed\n return yield* Effect.fail(new MyError());\n }).pipe(\n Effect.catchTag(\"MyError\", (error) =>\n Effect.logInfo(`Error occurred: ${error._tag}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `Effect.succeed` for values you already have, and `Effect.fail` for\nimmediate, known errors.\n\n## Anti-Pattern\n\nDo not wrap a static value in `Effect.sync`. While it works, `Effect.succeed`\nis more descriptive and direct for values that are already available." - }, - { - "id": "domain-modeling-tagged-errors", - "title": "Create Type-Safe Errors", - "description": "Use Data.TaggedError to create typed, distinguishable errors for your domain.", - "skillLevel": "beginner", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Effect, Data } from \"effect\"\n\n// ============================================\n// 1. Define tagged errors for your domain\n// ============================================\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n readonly userId: string\n}> {}\n\nclass InvalidEmailError extends Data.TaggedError(\"InvalidEmailError\")<{\n readonly email: string\n readonly reason: string\n}> {}\n\nclass DuplicateUserError extends Data.TaggedError(\"DuplicateUserError\")<{\n readonly email: string\n}> {}\n\n// ============================================\n// 2. Use in Effect functions\n// ============================================\n\ninterface User {\n id: string\n email: string\n name: string\n}\n\nconst validateEmail = (email: string): Effect.Effect => {\n if (!email.includes(\"@\")) {\n return Effect.fail(new InvalidEmailError({\n email,\n reason: \"Missing @ symbol\"\n }))\n }\n return Effect.succeed(email)\n}\n\nconst findUser = (id: string): Effect.Effect => {\n // Simulate database lookup\n if (id === \"123\") {\n return Effect.succeed({ id, email: \"alice@example.com\", name: \"Alice\" })\n }\n return Effect.fail(new UserNotFoundError({ userId: id }))\n}\n\nconst createUser = (\n email: string,\n name: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const validEmail = yield* validateEmail(email)\n\n // Simulate duplicate check\n if (validEmail === \"taken@example.com\") {\n return yield* Effect.fail(new DuplicateUserError({ email: validEmail }))\n }\n\n return {\n id: crypto.randomUUID(),\n email: validEmail,\n name,\n }\n })\n\n// ============================================\n// 3. Handle errors by tag\n// ============================================\n\nconst program = createUser(\"alice@example.com\", \"Alice\").pipe(\n Effect.catchTag(\"InvalidEmailError\", (error) =>\n Effect.succeed({\n id: \"fallback\",\n email: \"default@example.com\",\n name: `${error.email} was invalid: ${error.reason}`,\n })\n ),\n Effect.catchTag(\"DuplicateUserError\", (error) =>\n Effect.fail(new Error(`Email ${error.email} already registered`))\n )\n)\n\n// ============================================\n// 4. Match on all errors\n// ============================================\n\nconst handleAllErrors = createUser(\"bad-email\", \"Bob\").pipe(\n Effect.catchTags({\n InvalidEmailError: (e) => Effect.succeed(`Invalid: ${e.reason}`),\n DuplicateUserError: (e) => Effect.succeed(`Duplicate: ${e.email}`),\n })\n)\n\n// ============================================\n// 5. Run and see results\n// ============================================\n\nEffect.runPromise(program)\n .then((user) => console.log(\"Created:\", user))\n .catch((error) => console.error(\"Failed:\", error))\n```", - "antiPattern": "", - "explanation": "Plain `Error` or string messages cause problems:\n\n1. **No type safety** - Can't know what errors a function might throw\n2. **Hard to handle** - Matching on error messages is fragile\n3. **Poor documentation** - Errors aren't part of the function signature\n\nTagged errors solve this by making errors typed and distinguishable.\n\n---", - "content": "## Guideline\n\nCreate domain-specific errors using `Data.TaggedError`. Each error type gets a unique `_tag` for pattern matching.\n\n---\n\n## Rationale\n\nPlain `Error` or string messages cause problems:\n\n1. **No type safety** - Can't know what errors a function might throw\n2. **Hard to handle** - Matching on error messages is fragile\n3. **Poor documentation** - Errors aren't part of the function signature\n\nTagged errors solve this by making errors typed and distinguishable.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Data } from \"effect\"\n\n// ============================================\n// 1. Define tagged errors for your domain\n// ============================================\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n readonly userId: string\n}> {}\n\nclass InvalidEmailError extends Data.TaggedError(\"InvalidEmailError\")<{\n readonly email: string\n readonly reason: string\n}> {}\n\nclass DuplicateUserError extends Data.TaggedError(\"DuplicateUserError\")<{\n readonly email: string\n}> {}\n\n// ============================================\n// 2. Use in Effect functions\n// ============================================\n\ninterface User {\n id: string\n email: string\n name: string\n}\n\nconst validateEmail = (email: string): Effect.Effect => {\n if (!email.includes(\"@\")) {\n return Effect.fail(new InvalidEmailError({\n email,\n reason: \"Missing @ symbol\"\n }))\n }\n return Effect.succeed(email)\n}\n\nconst findUser = (id: string): Effect.Effect => {\n // Simulate database lookup\n if (id === \"123\") {\n return Effect.succeed({ id, email: \"alice@example.com\", name: \"Alice\" })\n }\n return Effect.fail(new UserNotFoundError({ userId: id }))\n}\n\nconst createUser = (\n email: string,\n name: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const validEmail = yield* validateEmail(email)\n\n // Simulate duplicate check\n if (validEmail === \"taken@example.com\") {\n return yield* Effect.fail(new DuplicateUserError({ email: validEmail }))\n }\n\n return {\n id: crypto.randomUUID(),\n email: validEmail,\n name,\n }\n })\n\n// ============================================\n// 3. Handle errors by tag\n// ============================================\n\nconst program = createUser(\"alice@example.com\", \"Alice\").pipe(\n Effect.catchTag(\"InvalidEmailError\", (error) =>\n Effect.succeed({\n id: \"fallback\",\n email: \"default@example.com\",\n name: `${error.email} was invalid: ${error.reason}`,\n })\n ),\n Effect.catchTag(\"DuplicateUserError\", (error) =>\n Effect.fail(new Error(`Email ${error.email} already registered`))\n )\n)\n\n// ============================================\n// 4. Match on all errors\n// ============================================\n\nconst handleAllErrors = createUser(\"bad-email\", \"Bob\").pipe(\n Effect.catchTags({\n InvalidEmailError: (e) => Effect.succeed(`Invalid: ${e.reason}`),\n DuplicateUserError: (e) => Effect.succeed(`Duplicate: ${e.email}`),\n })\n)\n\n// ============================================\n// 5. Run and see results\n// ============================================\n\nEffect.runPromise(program)\n .then((user) => console.log(\"Created:\", user))\n .catch((error) => console.error(\"Failed:\", error))\n```\n\n## Key Benefits\n\n| Feature | Benefit |\n|---------|---------|\n| **`_tag` property** | Unique identifier for pattern matching |\n| **Type-safe payload** | Each error can carry relevant data |\n| **Exhaustive handling** | Compiler warns if you miss an error type |\n| **Self-documenting** | Function signature shows possible errors |\n\n## Error Hierarchy Example\n\n```typescript\n// Domain errors\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n field: string\n message: string\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n resource: string\n id: string\n}> {}\n\nclass UnauthorizedError extends Data.TaggedError(\"UnauthorizedError\")<{\n reason: string\n}> {}\n\n// Function shows exactly what can go wrong\nconst updateUser = (\n id: string,\n data: unknown\n): Effect.Effect => {\n // Implementation...\n}\n```" - }, - { - "id": "constructor-from-iterable", - "title": "Creating from Collections", - "description": "Use fromIterable and fromArray to lift collections into Streams or Effects for batch or streaming processing.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Stream, Effect } from \"effect\";\n\n// Stream: Create a stream from an array\nconst numbers = [1, 2, 3, 4];\nconst numberStream = Stream.fromIterable(numbers); // Stream\n\n// Stream: Create a stream from any iterable\nfunction* gen() {\n yield \"a\";\n yield \"b\";\n}\nconst letterStream = Stream.fromIterable(gen()); // Stream\n\n// Effect: Create an effect from an array of effects (batch)\nconst effects = [Effect.succeed(1), Effect.succeed(2)];\nconst batchEffect = Effect.all(effects); // Effect<[1, 2]>\n```\n\n**Explanation:**\n\n- `Stream.fromIterable` creates a stream from any array or iterable, enabling streaming and batch operations.\n- `Effect.all` (covered elsewhere) can be used to process arrays of effects in batch.", - "antiPattern": "Manually looping over collections and running effects or streams imperatively, which loses composability, error handling, and resource safety.", - "explanation": "Lifting collections into Streams or Effects allows you to process data in a composable, resource-safe, and potentially concurrent way. \nIt also enables you to use all of Effect's combinators for transformation, filtering, and error handling.", - "content": "# Creating from Collections\n\n## Guideline\n\nUse the `fromIterable` and `fromArray` constructors to create Streams or Effects from arrays, iterables, or other collections. \nThis is the foundation for batch processing, streaming, and working with large or dynamic data sources.\n\n## Rationale\n\nLifting collections into Streams or Effects allows you to process data in a composable, resource-safe, and potentially concurrent way. \nIt also enables you to use all of Effect's combinators for transformation, filtering, and error handling.\n\n## Good Example\n\n```typescript\nimport { Stream, Effect } from \"effect\";\n\n// Stream: Create a stream from an array\nconst numbers = [1, 2, 3, 4];\nconst numberStream = Stream.fromIterable(numbers); // Stream\n\n// Stream: Create a stream from any iterable\nfunction* gen() {\n yield \"a\";\n yield \"b\";\n}\nconst letterStream = Stream.fromIterable(gen()); // Stream\n\n// Effect: Create an effect from an array of effects (batch)\nconst effects = [Effect.succeed(1), Effect.succeed(2)];\nconst batchEffect = Effect.all(effects); // Effect<[1, 2]>\n```\n\n**Explanation:**\n\n- `Stream.fromIterable` creates a stream from any array or iterable, enabling streaming and batch operations.\n- `Effect.all` (covered elsewhere) can be used to process arrays of effects in batch.\n\n## Anti-Pattern\n\nManually looping over collections and running effects or streams imperatively, which loses composability, error handling, and resource safety." - }, - { - "id": "constructor-sync-async", - "title": "Creating from Synchronous and Callback Code", - "description": "Use sync and async to create Effects from synchronous or callback-based computations, making them composable and type-safe.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Synchronous: Wrap a computation that is guaranteed not to throw\nconst effectSync = Effect.sync(() => Math.random()); // Effect\n\n// Callback-based: Wrap a Node.js-style callback API\nfunction legacyReadFile(\n path: string,\n cb: (err: Error | null, data?: string) => void\n) {\n setTimeout(() => cb(null, \"file contents\"), 10);\n}\n\nconst effectAsync = Effect.async((resume) => {\n legacyReadFile(\"file.txt\", (err, data) => {\n if (err) resume(Effect.fail(err));\n else resume(Effect.succeed(data!));\n });\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.sync` is for synchronous computations that are guaranteed not to throw.\n- `Effect.async` is for integrating callback-based APIs, converting them into Effects.", - "antiPattern": "Directly calling synchronous or callback-based APIs inside Effects without lifting them, which can break composability and error handling.", - "explanation": "Many APIs are synchronous or use callbacks instead of Promises. \nBy lifting them into Effects, you gain access to all of Effect's combinators, error handling, and resource safety.", - "content": "# Creating from Synchronous and Callback Code\n\n## Guideline\n\nUse the `sync` and `async` constructors to lift synchronous or callback-based computations into the Effect world. \nThis enables safe, composable interop with legacy or third-party code that doesn't use Promises or Effects.\n\n## Rationale\n\nMany APIs are synchronous or use callbacks instead of Promises. \nBy lifting them into Effects, you gain access to all of Effect's combinators, error handling, and resource safety.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Synchronous: Wrap a computation that is guaranteed not to throw\nconst effectSync = Effect.sync(() => Math.random()); // Effect\n\n// Callback-based: Wrap a Node.js-style callback API\nfunction legacyReadFile(\n path: string,\n cb: (err: Error | null, data?: string) => void\n) {\n setTimeout(() => cb(null, \"file contents\"), 10);\n}\n\nconst effectAsync = Effect.async((resume) => {\n legacyReadFile(\"file.txt\", (err, data) => {\n if (err) resume(Effect.fail(err));\n else resume(Effect.succeed(data!));\n });\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.sync` is for synchronous computations that are guaranteed not to throw.\n- `Effect.async` is for integrating callback-based APIs, converting them into Effects.\n\n## Anti-Pattern\n\nDirectly calling synchronous or callback-based APIs inside Effects without lifting them, which can break composability and error handling." - }, - { - "id": "observability-debugging", - "title": "Debug Effect Programs", - "description": "Use Effect.tap and logging to inspect values without changing program flow.", - "skillLevel": "beginner", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, pipe } from \"effect\"\n\n// ============================================\n// 1. Using tap to inspect values\n// ============================================\n\nconst fetchUser = (id: string) =>\n Effect.succeed({ id, name: \"Alice\", email: \"alice@example.com\" })\n\nconst processUser = (id: string) =>\n fetchUser(id).pipe(\n // tap runs an effect for its side effect, then continues with original value\n Effect.tap((user) => Effect.log(`Fetched user: ${user.name}`)),\n Effect.map((user) => ({ ...user, processed: true })),\n Effect.tap((user) => Effect.log(`Processed: ${JSON.stringify(user)}`))\n )\n\n// ============================================\n// 2. Debug a pipeline\n// ============================================\n\nconst numbers = [1, 2, 3, 4, 5]\n\nconst pipeline = Effect.gen(function* () {\n yield* Effect.log(\"Starting pipeline\")\n\n const step1 = numbers.filter((n) => n % 2 === 0)\n yield* Effect.log(`After filter (even): ${JSON.stringify(step1)}`)\n\n const step2 = step1.map((n) => n * 10)\n yield* Effect.log(`After map (*10): ${JSON.stringify(step2)}`)\n\n const step3 = step2.reduce((a, b) => a + b, 0)\n yield* Effect.log(`After reduce (sum): ${step3}`)\n\n return step3\n})\n\n// ============================================\n// 3. Debug errors\n// ============================================\n\nconst riskyOperation = (shouldFail: boolean) =>\n Effect.gen(function* () {\n yield* Effect.log(\"Starting risky operation\")\n\n if (shouldFail) {\n yield* Effect.log(\"About to fail...\")\n return yield* Effect.fail(new Error(\"Something went wrong\"))\n }\n\n yield* Effect.log(\"Success!\")\n return \"result\"\n })\n\nconst debugErrors = riskyOperation(true).pipe(\n // Log when operation fails\n Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)),\n\n // Provide a fallback\n Effect.catchAll((error) => {\n return Effect.succeed(`Recovered from: ${error.message}`)\n })\n)\n\n// ============================================\n// 4. Trace execution flow\n// ============================================\n\nconst step = (name: string, value: number) =>\n Effect.gen(function* () {\n yield* Effect.log(`[${name}] Input: ${value}`)\n const result = value * 2\n yield* Effect.log(`[${name}] Output: ${result}`)\n return result\n })\n\nconst tracedWorkflow = Effect.gen(function* () {\n const a = yield* step(\"Step 1\", 5)\n const b = yield* step(\"Step 2\", a)\n const c = yield* step(\"Step 3\", b)\n yield* Effect.log(`Final result: ${c}`)\n return c\n})\n\n// ============================================\n// 5. Quick debug with console\n// ============================================\n\n// Sometimes you just need console.log\nconst quickDebug = Effect.gen(function* () {\n const value = yield* Effect.succeed(42)\n \n // Effect.sync wraps side effects\n yield* Effect.sync(() => console.log(\"Quick debug:\", value))\n \n return value\n})\n\n// ============================================\n// 6. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Tap Example ===\")\n yield* processUser(\"123\")\n\n yield* Effect.log(\"\\n=== Pipeline Debug ===\")\n yield* pipeline\n\n yield* Effect.log(\"\\n=== Error Debug ===\")\n yield* debugErrors\n\n yield* Effect.log(\"\\n=== Traced Workflow ===\")\n yield* tracedWorkflow\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Debugging Effect code differs from imperative code:\n\n1. **No breakpoints** - Effects are descriptions, not executions\n2. **Lazy evaluation** - Code runs later when you call `runPromise`\n3. **Composition** - Effects chain together\n\n`tap` and logging let you see inside without breaking the chain.\n\n---", - "content": "## Guideline\n\nUse `Effect.tap` to inspect values and `Effect.log` to trace execution without changing program behavior.\n\n---\n\n## Rationale\n\nDebugging Effect code differs from imperative code:\n\n1. **No breakpoints** - Effects are descriptions, not executions\n2. **Lazy evaluation** - Code runs later when you call `runPromise`\n3. **Composition** - Effects chain together\n\n`tap` and logging let you see inside without breaking the chain.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, pipe } from \"effect\"\n\n// ============================================\n// 1. Using tap to inspect values\n// ============================================\n\nconst fetchUser = (id: string) =>\n Effect.succeed({ id, name: \"Alice\", email: \"alice@example.com\" })\n\nconst processUser = (id: string) =>\n fetchUser(id).pipe(\n // tap runs an effect for its side effect, then continues with original value\n Effect.tap((user) => Effect.log(`Fetched user: ${user.name}`)),\n Effect.map((user) => ({ ...user, processed: true })),\n Effect.tap((user) => Effect.log(`Processed: ${JSON.stringify(user)}`))\n )\n\n// ============================================\n// 2. Debug a pipeline\n// ============================================\n\nconst numbers = [1, 2, 3, 4, 5]\n\nconst pipeline = Effect.gen(function* () {\n yield* Effect.log(\"Starting pipeline\")\n\n const step1 = numbers.filter((n) => n % 2 === 0)\n yield* Effect.log(`After filter (even): ${JSON.stringify(step1)}`)\n\n const step2 = step1.map((n) => n * 10)\n yield* Effect.log(`After map (*10): ${JSON.stringify(step2)}`)\n\n const step3 = step2.reduce((a, b) => a + b, 0)\n yield* Effect.log(`After reduce (sum): ${step3}`)\n\n return step3\n})\n\n// ============================================\n// 3. Debug errors\n// ============================================\n\nconst riskyOperation = (shouldFail: boolean) =>\n Effect.gen(function* () {\n yield* Effect.log(\"Starting risky operation\")\n\n if (shouldFail) {\n yield* Effect.log(\"About to fail...\")\n return yield* Effect.fail(new Error(\"Something went wrong\"))\n }\n\n yield* Effect.log(\"Success!\")\n return \"result\"\n })\n\nconst debugErrors = riskyOperation(true).pipe(\n // Log when operation fails\n Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)),\n\n // Provide a fallback\n Effect.catchAll((error) => {\n return Effect.succeed(`Recovered from: ${error.message}`)\n })\n)\n\n// ============================================\n// 4. Trace execution flow\n// ============================================\n\nconst step = (name: string, value: number) =>\n Effect.gen(function* () {\n yield* Effect.log(`[${name}] Input: ${value}`)\n const result = value * 2\n yield* Effect.log(`[${name}] Output: ${result}`)\n return result\n })\n\nconst tracedWorkflow = Effect.gen(function* () {\n const a = yield* step(\"Step 1\", 5)\n const b = yield* step(\"Step 2\", a)\n const c = yield* step(\"Step 3\", b)\n yield* Effect.log(`Final result: ${c}`)\n return c\n})\n\n// ============================================\n// 5. Quick debug with console\n// ============================================\n\n// Sometimes you just need console.log\nconst quickDebug = Effect.gen(function* () {\n const value = yield* Effect.succeed(42)\n \n // Effect.sync wraps side effects\n yield* Effect.sync(() => console.log(\"Quick debug:\", value))\n \n return value\n})\n\n// ============================================\n// 6. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Tap Example ===\")\n yield* processUser(\"123\")\n\n yield* Effect.log(\"\\n=== Pipeline Debug ===\")\n yield* pipeline\n\n yield* Effect.log(\"\\n=== Error Debug ===\")\n yield* debugErrors\n\n yield* Effect.log(\"\\n=== Traced Workflow ===\")\n yield* tracedWorkflow\n})\n\nEffect.runPromise(program)\n```\n\n## Debugging Tools\n\n| Tool | Purpose |\n|------|---------|\n| `Effect.tap` | Inspect success value |\n| `Effect.tapError` | Inspect error value |\n| `Effect.tapBoth` | Inspect both success and error |\n| `Effect.log` | Log messages |\n| `Effect.annotateLogs` | Add context to logs |\n\n## Tips\n\n1. **Add logging liberally** - Remove it later\n2. **Use tap, not map** - Don't accidentally transform values\n3. **Log before and after** - See what changed\n4. **Include context** - Log the inputs, not just \"processing...\"\n5. **Use withLogSpan** - Find slow operations" - }, - { - "id": "decouple-fibers-with-queue-pubsub", - "title": "Decouple Fibers with Queues and PubSub", - "description": "Use Queue for point-to-point work distribution and PubSub for broadcast messaging between fibers.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "A producer fiber adds jobs to a `Queue`, and a worker fiber takes jobs off the queue to process them.\n\n```typescript\nimport { Effect, Queue, Fiber } from \"effect\";\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting queue demo...\");\n\n // Create a bounded queue that can hold a maximum of 10 items.\n // This prevents memory issues by applying backpressure when the queue is full.\n // If a producer tries to add to a full queue, it will suspend until space is available.\n const queue = yield* Queue.bounded(10);\n yield* Effect.logInfo(\"Created bounded queue\");\n\n // Producer Fiber: Add a job to the queue every second.\n // This fiber runs independently and continuously produces work items.\n // The producer-consumer pattern decouples work generation from work processing.\n const producer = yield* Effect.gen(function* () {\n let i = 0;\n while (true) {\n const job = `job-${i++}`;\n yield* Effect.logInfo(`Producing ${job}...`);\n\n // Queue.offer adds an item to the queue. If the queue is full,\n // this operation will suspend the fiber until space becomes available.\n // This provides natural backpressure control.\n yield* Queue.offer(queue, job);\n\n // Sleep for 500ms between job creation. This controls the production rate.\n // Producer is faster than consumer (500ms vs 1000ms) to demonstrate queue buffering.\n yield* Effect.sleep(\"500 millis\");\n }\n }).pipe(Effect.fork); // Fork creates a new fiber that runs concurrently\n\n yield* Effect.logInfo(\"Started producer fiber\");\n\n // Worker Fiber: Take a job from the queue and process it.\n // This fiber runs independently and processes work items as they become available.\n // Multiple workers could be created to scale processing capacity.\n const worker = yield* Effect.gen(function* () {\n while (true) {\n // Queue.take removes and returns an item from the queue.\n // If the queue is empty, this operation will suspend the fiber\n // until an item becomes available. This prevents busy-waiting.\n const job = yield* Queue.take(queue);\n yield* Effect.logInfo(`Processing ${job}...`);\n\n // Simulate work by sleeping for 1 second.\n // This makes the worker slower than the producer, causing queue buildup.\n yield* Effect.sleep(\"1 second\");\n yield* Effect.logInfo(`Completed ${job}`);\n }\n }).pipe(Effect.fork); // Fork creates another independent fiber\n\n yield* Effect.logInfo(\"Started worker fiber\");\n\n // Let them run for a while...\n // The main fiber sleeps while the producer and worker fibers run concurrently.\n // During this time, you'll see the queue acting as a buffer between\n // the fast producer and slow worker.\n yield* Effect.logInfo(\"Running for 10 seconds...\");\n yield* Effect.sleep(\"10 seconds\");\n yield* Effect.logInfo(\"Done!\");\n\n // Interrupt both fibers to clean up resources.\n // Fiber.interrupt sends an interruption signal to the fiber,\n // allowing it to perform cleanup operations before terminating.\n // This is safer than forcefully killing fibers.\n yield* Fiber.interrupt(producer);\n yield* Fiber.interrupt(worker);\n\n // Note: In a real application, you might want to:\n // 1. Drain the queue before interrupting workers\n // 2. Use Fiber.join to wait for graceful shutdown\n // 3. Handle interruption signals in the fiber loops\n});\n\n// Run the program\n// This demonstrates the producer-consumer pattern with Effect fibers:\n// - Fibers are lightweight threads that can be created in large numbers\n// - Queues provide safe communication between fibers\n// - Backpressure prevents resource exhaustion\n// - Interruption allows for graceful shutdown\nEffect.runPromise(program);\n```\n\n\nA publisher sends an event, and multiple subscribers react to it independently.\n\n```typescript\nimport { Effect, PubSub } from \"effect\";\n\nconst program = Effect.gen(function* () {\n const pubsub = yield* PubSub.bounded(10);\n\n // Subscriber 1: The \"Audit\" service\n const auditSub = PubSub.subscribe(pubsub).pipe(\n Effect.flatMap((subscription) =>\n Effect.gen(function* () {\n while (true) {\n const event = yield* Queue.take(subscription);\n yield* Effect.log(`AUDIT: Received event: ${event}`);\n }\n })\n ),\n Effect.fork\n );\n\n // Subscriber 2: The \"Notifier\" service\n const notifierSub = PubSub.subscribe(pubsub).pipe(\n Effect.flatMap((subscription) =>\n Effect.gen(function* () {\n while (true) {\n const event = yield* Queue.take(subscription);\n yield* Effect.log(`NOTIFIER: Sending notification for: ${event}`);\n }\n })\n ),\n Effect.fork\n );\n\n // Give subscribers time to start\n yield* Effect.sleep(\"1 second\");\n\n // Publisher: Publish an event that both subscribers will receive.\n yield* PubSub.publish(pubsub, \"user_logged_in\");\n});\n```\n\n---", - "antiPattern": "Simulating a queue with a simple `Ref`. This approach is inefficient due to polling and is not safe from race conditions without manual, complex locking mechanisms. It also lacks critical features like back-pressure.\n\n```typescript\nimport { Effect, Ref } from \"effect\";\n\n// ❌ WRONG: This is inefficient and prone to race conditions.\nconst program = Effect.gen(function* () {\n const queueRef = yield* Ref.make([]);\n\n // Producer adds to the array\n const producer = Ref.update(queueRef, (q) => [...q, \"new_item\"]);\n\n // Consumer has to constantly poll the array to see if it's empty.\n const consumer = Ref.get(queueRef).pipe(\n Effect.flatMap(\n (q) =>\n q.length > 0\n ? Ref.set(queueRef, q.slice(1)).pipe(Effect.as(q[0]))\n : Effect.sleep(\"1 second\").pipe(Effect.flatMap(() => consumer)) // Inefficient polling\n )\n );\n});\n```", - "explanation": "Directly calling functions between different logical parts of a concurrent application creates tight coupling, making the system brittle and hard to scale. `Queue` and `PubSub` solve this by acting as asynchronous, fiber-safe message brokers.\n\nThis decouples the **producer** of a message from its **consumer(s)**. The producer doesn't need to know who is listening, or how many listeners there are. This allows you to build resilient, scalable systems where you can add or remove workers/listeners without changing the producer's code.\n\nFurthermore, bounded `Queue`s and `PubSub`s provide automatic **back-pressure**. If consumers can't keep up, the producer will automatically pause before adding new items, preventing your system from becoming overloaded.\n\n---", - "content": "## Guideline\n\nTo enable communication between independent, concurrent fibers, use one of Effect's specialized data structures:\n\n- **`Queue`**: For distributing work items. Each item put on the queue is taken and processed by only **one** consumer.\n- **`PubSub`**: For broadcasting events. Each message published is delivered to **every** subscriber.\n\n---\n\n## Rationale\n\nDirectly calling functions between different logical parts of a concurrent application creates tight coupling, making the system brittle and hard to scale. `Queue` and `PubSub` solve this by acting as asynchronous, fiber-safe message brokers.\n\nThis decouples the **producer** of a message from its **consumer(s)**. The producer doesn't need to know who is listening, or how many listeners there are. This allows you to build resilient, scalable systems where you can add or remove workers/listeners without changing the producer's code.\n\nFurthermore, bounded `Queue`s and `PubSub`s provide automatic **back-pressure**. If consumers can't keep up, the producer will automatically pause before adding new items, preventing your system from becoming overloaded.\n\n---\n\n## Good Example 1: `Queue` for a Work Pool\n\nA producer fiber adds jobs to a `Queue`, and a worker fiber takes jobs off the queue to process them.\n\n```typescript\nimport { Effect, Queue, Fiber } from \"effect\";\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting queue demo...\");\n\n // Create a bounded queue that can hold a maximum of 10 items.\n // This prevents memory issues by applying backpressure when the queue is full.\n // If a producer tries to add to a full queue, it will suspend until space is available.\n const queue = yield* Queue.bounded(10);\n yield* Effect.logInfo(\"Created bounded queue\");\n\n // Producer Fiber: Add a job to the queue every second.\n // This fiber runs independently and continuously produces work items.\n // The producer-consumer pattern decouples work generation from work processing.\n const producer = yield* Effect.gen(function* () {\n let i = 0;\n while (true) {\n const job = `job-${i++}`;\n yield* Effect.logInfo(`Producing ${job}...`);\n\n // Queue.offer adds an item to the queue. If the queue is full,\n // this operation will suspend the fiber until space becomes available.\n // This provides natural backpressure control.\n yield* Queue.offer(queue, job);\n\n // Sleep for 500ms between job creation. This controls the production rate.\n // Producer is faster than consumer (500ms vs 1000ms) to demonstrate queue buffering.\n yield* Effect.sleep(\"500 millis\");\n }\n }).pipe(Effect.fork); // Fork creates a new fiber that runs concurrently\n\n yield* Effect.logInfo(\"Started producer fiber\");\n\n // Worker Fiber: Take a job from the queue and process it.\n // This fiber runs independently and processes work items as they become available.\n // Multiple workers could be created to scale processing capacity.\n const worker = yield* Effect.gen(function* () {\n while (true) {\n // Queue.take removes and returns an item from the queue.\n // If the queue is empty, this operation will suspend the fiber\n // until an item becomes available. This prevents busy-waiting.\n const job = yield* Queue.take(queue);\n yield* Effect.logInfo(`Processing ${job}...`);\n\n // Simulate work by sleeping for 1 second.\n // This makes the worker slower than the producer, causing queue buildup.\n yield* Effect.sleep(\"1 second\");\n yield* Effect.logInfo(`Completed ${job}`);\n }\n }).pipe(Effect.fork); // Fork creates another independent fiber\n\n yield* Effect.logInfo(\"Started worker fiber\");\n\n // Let them run for a while...\n // The main fiber sleeps while the producer and worker fibers run concurrently.\n // During this time, you'll see the queue acting as a buffer between\n // the fast producer and slow worker.\n yield* Effect.logInfo(\"Running for 10 seconds...\");\n yield* Effect.sleep(\"10 seconds\");\n yield* Effect.logInfo(\"Done!\");\n\n // Interrupt both fibers to clean up resources.\n // Fiber.interrupt sends an interruption signal to the fiber,\n // allowing it to perform cleanup operations before terminating.\n // This is safer than forcefully killing fibers.\n yield* Fiber.interrupt(producer);\n yield* Fiber.interrupt(worker);\n\n // Note: In a real application, you might want to:\n // 1. Drain the queue before interrupting workers\n // 2. Use Fiber.join to wait for graceful shutdown\n // 3. Handle interruption signals in the fiber loops\n});\n\n// Run the program\n// This demonstrates the producer-consumer pattern with Effect fibers:\n// - Fibers are lightweight threads that can be created in large numbers\n// - Queues provide safe communication between fibers\n// - Backpressure prevents resource exhaustion\n// - Interruption allows for graceful shutdown\nEffect.runPromise(program);\n```\n\n## Good Example 2: `PubSub` for Event Broadcasting\n\nA publisher sends an event, and multiple subscribers react to it independently.\n\n```typescript\nimport { Effect, PubSub } from \"effect\";\n\nconst program = Effect.gen(function* () {\n const pubsub = yield* PubSub.bounded(10);\n\n // Subscriber 1: The \"Audit\" service\n const auditSub = PubSub.subscribe(pubsub).pipe(\n Effect.flatMap((subscription) =>\n Effect.gen(function* () {\n while (true) {\n const event = yield* Queue.take(subscription);\n yield* Effect.log(`AUDIT: Received event: ${event}`);\n }\n })\n ),\n Effect.fork\n );\n\n // Subscriber 2: The \"Notifier\" service\n const notifierSub = PubSub.subscribe(pubsub).pipe(\n Effect.flatMap((subscription) =>\n Effect.gen(function* () {\n while (true) {\n const event = yield* Queue.take(subscription);\n yield* Effect.log(`NOTIFIER: Sending notification for: ${event}`);\n }\n })\n ),\n Effect.fork\n );\n\n // Give subscribers time to start\n yield* Effect.sleep(\"1 second\");\n\n // Publisher: Publish an event that both subscribers will receive.\n yield* PubSub.publish(pubsub, \"user_logged_in\");\n});\n```\n\n---\n\n## Anti-Pattern\n\nSimulating a queue with a simple `Ref`. This approach is inefficient due to polling and is not safe from race conditions without manual, complex locking mechanisms. It also lacks critical features like back-pressure.\n\n```typescript\nimport { Effect, Ref } from \"effect\";\n\n// ❌ WRONG: This is inefficient and prone to race conditions.\nconst program = Effect.gen(function* () {\n const queueRef = yield* Ref.make([]);\n\n // Producer adds to the array\n const producer = Ref.update(queueRef, (q) => [...q, \"new_item\"]);\n\n // Consumer has to constantly poll the array to see if it's empty.\n const consumer = Ref.get(queueRef).pipe(\n Effect.flatMap(\n (q) =>\n q.length > 0\n ? Ref.set(queueRef, q.slice(1)).pipe(Effect.as(q[0]))\n : Effect.sleep(\"1 second\").pipe(Effect.flatMap(() => consumer)) // Inefficient polling\n )\n );\n});\n```" - }, - { - "id": "define-config-schema", - "title": "Define a Type-Safe Configuration Schema", - "description": "Define a type-safe configuration schema.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Config, Effect, ConfigProvider, Layer } from \"effect\";\n\nconst ServerConfig = Config.nested(\"SERVER\")(\n Config.all({\n host: Config.string(\"HOST\"),\n port: Config.number(\"PORT\"),\n })\n);\n\n// Example program that uses the config\nconst program = Effect.gen(function* () {\n const config = yield* ServerConfig;\n yield* Effect.logInfo(`Server config loaded: ${JSON.stringify(config)}`);\n});\n\n// Create a config provider with test values\nconst TestConfig = ConfigProvider.fromMap(\n new Map([\n [\"SERVER.HOST\", \"localhost\"],\n [\"SERVER.PORT\", \"3000\"],\n ])\n);\n\n// Run with test config\nEffect.runPromise(Effect.provide(program, Layer.setConfigProvider(TestConfig)));\n```\n\n**Explanation:** \nThis schema ensures that both `host` and `port` are present and properly typed, and that their source is clearly defined.", - "antiPattern": "Directly accessing `process.env`. This is not type-safe, scatters configuration access throughout your codebase, and can lead to parsing errors or `undefined` values.", - "explanation": "This creates a single, type-safe source of truth for your configuration, eliminating runtime errors from missing or malformed environment variables and making the required configuration explicit.", - "content": "# Define a Type-Safe Configuration Schema\n\n## Guideline\n\nDefine all external configuration values your application needs using the schema-building functions from `Effect.Config`, such as `Config.string` and `Config.number`.\n\n## Rationale\n\nThis creates a single, type-safe source of truth for your configuration, eliminating runtime errors from missing or malformed environment variables and making the required configuration explicit.\n\n## Good Example\n\n```typescript\nimport { Config, Effect, ConfigProvider, Layer } from \"effect\";\n\nconst ServerConfig = Config.nested(\"SERVER\")(\n Config.all({\n host: Config.string(\"HOST\"),\n port: Config.number(\"PORT\"),\n })\n);\n\n// Example program that uses the config\nconst program = Effect.gen(function* () {\n const config = yield* ServerConfig;\n yield* Effect.logInfo(`Server config loaded: ${JSON.stringify(config)}`);\n});\n\n// Create a config provider with test values\nconst TestConfig = ConfigProvider.fromMap(\n new Map([\n [\"SERVER.HOST\", \"localhost\"],\n [\"SERVER.PORT\", \"3000\"],\n ])\n);\n\n// Run with test config\nEffect.runPromise(Effect.provide(program, Layer.setConfigProvider(TestConfig)));\n```\n\n**Explanation:** \nThis schema ensures that both `host` and `port` are present and properly typed, and that their source is clearly defined.\n\n## Anti-Pattern\n\nDirectly accessing `process.env`. This is not type-safe, scatters configuration access throughout your codebase, and can lead to parsing errors or `undefined` values." - }, - { - "id": "define-contracts-with-schema", - "title": "Define Contracts Upfront with Schema", - "description": "Define contracts upfront with schema.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Schema, Effect, Data } from \"effect\";\n\n// Define User schema and type\nconst UserSchema = Schema.Struct({\n id: Schema.Number,\n name: Schema.String,\n});\n\ntype User = Schema.Schema.Type;\n\n// Define error type\nclass UserNotFound extends Data.TaggedError(\"UserNotFound\")<{\n readonly id: number;\n}> {}\n\n// Create database service implementation\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: number) =>\n id === 1\n ? Effect.succeed({ id: 1, name: \"John\" })\n : Effect.fail(new UserNotFound({ id })),\n }),\n}) {}\n\n// Create a program that demonstrates schema and error handling\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n\n // Try to get an existing user\n yield* Effect.logInfo(\"Looking up user 1...\");\n const user1 = yield* db.getUser(1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user1)}`);\n\n // Try to get a non-existent user\n yield* Effect.logInfo(\"\\nLooking up user 999...\");\n yield* Effect.logInfo(\"Attempting to get user 999...\");\n yield* Effect.gen(function* () {\n const user = yield* db.getUser(999);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) => {\n if (error instanceof UserNotFound) {\n return Effect.logInfo(`Error: User with id ${error.id} not found`);\n }\n return Effect.logInfo(`Unexpected error: ${error}`);\n })\n );\n\n // Try to decode invalid data\n yield* Effect.logInfo(\"\\nTrying to decode invalid user data...\");\n const invalidUser = { id: \"not-a-number\", name: 123 } as any;\n yield* Effect.gen(function* () {\n const user = yield* Schema.decode(UserSchema)(invalidUser);\n yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logInfo(`Validation failed:\\n${JSON.stringify(error, null, 2)}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, Database.Default));\n```\n\n**Explanation:** \nDefining schemas upfront clarifies your contracts and ensures both type safety\nand runtime validation.", - "antiPattern": "Defining logic with implicit `any` types first and adding validation later as\nan afterthought. This leads to brittle code that lacks a clear contract.", - "explanation": "This \"schema-first\" approach separates the \"what\" (the data shape) from the\n\"how\" (the implementation). It provides a single source of truth for both\ncompile-time static types and runtime validation.", - "content": "# Define Contracts Upfront with Schema\n\n## Guideline\n\nBefore writing implementation logic, define the shape of your data models and\nfunction signatures using `Effect/Schema`.\n\n## Rationale\n\nThis \"schema-first\" approach separates the \"what\" (the data shape) from the\n\"how\" (the implementation). It provides a single source of truth for both\ncompile-time static types and runtime validation.\n\n## Good Example\n\n```typescript\nimport { Schema, Effect, Data } from \"effect\";\n\n// Define User schema and type\nconst UserSchema = Schema.Struct({\n id: Schema.Number,\n name: Schema.String,\n});\n\ntype User = Schema.Schema.Type;\n\n// Define error type\nclass UserNotFound extends Data.TaggedError(\"UserNotFound\")<{\n readonly id: number;\n}> {}\n\n// Create database service implementation\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: number) =>\n id === 1\n ? Effect.succeed({ id: 1, name: \"John\" })\n : Effect.fail(new UserNotFound({ id })),\n }),\n}) {}\n\n// Create a program that demonstrates schema and error handling\nconst program = Effect.gen(function* () {\n const db = yield* Database;\n\n // Try to get an existing user\n yield* Effect.logInfo(\"Looking up user 1...\");\n const user1 = yield* db.getUser(1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user1)}`);\n\n // Try to get a non-existent user\n yield* Effect.logInfo(\"\\nLooking up user 999...\");\n yield* Effect.logInfo(\"Attempting to get user 999...\");\n yield* Effect.gen(function* () {\n const user = yield* db.getUser(999);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) => {\n if (error instanceof UserNotFound) {\n return Effect.logInfo(`Error: User with id ${error.id} not found`);\n }\n return Effect.logInfo(`Unexpected error: ${error}`);\n })\n );\n\n // Try to decode invalid data\n yield* Effect.logInfo(\"\\nTrying to decode invalid user data...\");\n const invalidUser = { id: \"not-a-number\", name: 123 } as any;\n yield* Effect.gen(function* () {\n const user = yield* Schema.decode(UserSchema)(invalidUser);\n yield* Effect.logInfo(`Decoded user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logInfo(`Validation failed:\\n${JSON.stringify(error, null, 2)}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, Database.Default));\n```\n\n**Explanation:** \nDefining schemas upfront clarifies your contracts and ensures both type safety\nand runtime validation.\n\n## Anti-Pattern\n\nDefining logic with implicit `any` types first and adding validation later as\nan afterthought. This leads to brittle code that lacks a clear contract." - }, - { - "id": "define-tagged-errors", - "title": "Define Type-Safe Errors with Data.TaggedError", - "description": "Define type-safe errors with Data.TaggedError.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define our tagged error type\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n readonly cause: unknown;\n}> {}\n\n// Function that simulates a database error\nconst findUser = (\n id: number\n): Effect.Effect<{ id: number; name: string }, DatabaseError> =>\n Effect.gen(function* () {\n if (id < 0) {\n return yield* Effect.fail(new DatabaseError({ cause: \"Invalid ID\" }));\n }\n return { id, name: `User ${id}` };\n });\n\n// Create a program that demonstrates error handling\nconst program = Effect.gen(function* () {\n // Try to find a valid user\n yield* Effect.logInfo(\"Looking up user 1...\");\n yield* Effect.gen(function* () {\n const user = yield* findUser(1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logInfo(`Error finding user: ${error._tag} - ${error.cause}`)\n )\n );\n\n // Try to find an invalid user\n yield* Effect.logInfo(\"\\nLooking up user -1...\");\n yield* Effect.gen(function* () {\n const user = yield* findUser(-1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchTag(\"DatabaseError\", (error) =>\n Effect.logInfo(`Database error: ${error._tag} - ${error.cause}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:** \nTagged errors allow you to handle errors in a type-safe, self-documenting way.", - "antiPattern": "Using generic `Error` objects or strings in the error channel. This loses all\ntype information, forcing consumers to use `catchAll` and perform unsafe\nchecks.", - "explanation": "This gives each error a unique, literal `_tag` that Effect can use for type\ndiscrimination with `Effect.catchTag`, making your error handling fully\ntype-safe.", - "content": "# Define Type-Safe Errors with Data.TaggedError\n\n## Guideline\n\nFor any distinct failure mode in your application, define a custom error class\nthat extends `Data.TaggedError`.\n\n## Rationale\n\nThis gives each error a unique, literal `_tag` that Effect can use for type\ndiscrimination with `Effect.catchTag`, making your error handling fully\ntype-safe.\n\n## Good Example\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define our tagged error type\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n readonly cause: unknown;\n}> {}\n\n// Function that simulates a database error\nconst findUser = (\n id: number\n): Effect.Effect<{ id: number; name: string }, DatabaseError> =>\n Effect.gen(function* () {\n if (id < 0) {\n return yield* Effect.fail(new DatabaseError({ cause: \"Invalid ID\" }));\n }\n return { id, name: `User ${id}` };\n });\n\n// Create a program that demonstrates error handling\nconst program = Effect.gen(function* () {\n // Try to find a valid user\n yield* Effect.logInfo(\"Looking up user 1...\");\n yield* Effect.gen(function* () {\n const user = yield* findUser(1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logInfo(`Error finding user: ${error._tag} - ${error.cause}`)\n )\n );\n\n // Try to find an invalid user\n yield* Effect.logInfo(\"\\nLooking up user -1...\");\n yield* Effect.gen(function* () {\n const user = yield* findUser(-1);\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n }).pipe(\n Effect.catchTag(\"DatabaseError\", (error) =>\n Effect.logInfo(`Database error: ${error._tag} - ${error.cause}`)\n )\n );\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:** \nTagged errors allow you to handle errors in a type-safe, self-documenting way.\n\n## Anti-Pattern\n\nUsing generic `Error` objects or strings in the error channel. This loses all\ntype information, forcing consumers to use `catchAll` and perform unsafe\nchecks." - }, - { - "id": "distinguish-not-found-from-errors", - "title": "Distinguish 'Not Found' from Errors", - "description": "Use Effect> to distinguish between recoverable 'not found' cases and actual failures.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "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.\n\n```typescript\nimport { Effect, Option, Data } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\") {}\n\n// This signature is extremely honest about its possible outcomes.\nconst findUserInDb = (\n id: number\n): Effect.Effect, DatabaseError> =>\n Effect.gen(function* () {\n // This could fail with a DatabaseError\n const dbResult = yield* Effect.try({\n try: () => (id === 1 ? { id: 1, name: \"Paul\" } : null),\n catch: () => new DatabaseError(),\n });\n\n // We wrap the potentially null result in an Option\n return Option.fromNullable(dbResult);\n });\n\n// The caller can now handle all three cases explicitly.\nconst program = (id: number) =>\n findUserInDb(id).pipe(\n Effect.flatMap((maybeUser) =>\n Option.match(maybeUser, {\n onNone: () =>\n Effect.logInfo(`Result: User with ID ${id} was not found.`),\n onSome: (user) => Effect.logInfo(`Result: Found user ${user.name}.`),\n })\n ),\n Effect.catchAll((error) =>\n Effect.logInfo(\"Error: Could not connect to the database.\")\n )\n );\n\n// Run the program with different IDs\nEffect.runPromise(\n Effect.gen(function* () {\n // Try with existing user\n yield* Effect.logInfo(\"Looking for user with ID 1...\");\n yield* program(1);\n\n // Try with non-existent user\n yield* Effect.logInfo(\"\\nLooking for user with ID 2...\");\n yield* program(2);\n })\n);\n```", - "antiPattern": "A common alternative is to create a specific NotFoundError and put it in the error channel alongside other errors.\n\n```typescript\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\") {}\n\n// ❌ This signature conflates two different kinds of failure.\nconst findUserUnsafely = (\n id: number\n): Effect.Effect => {\n // ...\n return Effect.fail(new NotFoundError());\n};\n```\n\nWhile this works, it can be less expressive. It treats a \"not found\" result—which might be a normal part of your application's flow—the same as a catastrophic DatabaseError.\n\nUsing `Effect>` often leads to clearer and more precise business logic.", - "explanation": "This pattern provides a precise way to handle three distinct outcomes of an operation:\n\n1. **Success with a value:** `Effect.succeed(Option.some(value))`\n2. **Success with no value:** `Effect.succeed(Option.none())` (e.g., user not found)\n3. **Failure:** `Effect.fail(new DatabaseError())` (e.g., database connection lost)\n\nBy using `Option` inside the success channel of an `Effect`, you keep the error channel clean for true, unexpected, or unrecoverable errors. The \"not found\" case is often an expected and recoverable part of your business logic, and `Option.none()` models this perfectly.\n\n---", - "content": "## Guideline\n\nWhen a computation can fail (e.g., a network error) or succeed but find nothing, model its return type as `Effect>`. This separates the \"hard failure\" channel from the \"soft failure\" (or empty) channel.\n\n---\n\n## Rationale\n\nThis pattern provides a precise way to handle three distinct outcomes of an operation:\n\n1. **Success with a value:** `Effect.succeed(Option.some(value))`\n2. **Success with no value:** `Effect.succeed(Option.none())` (e.g., user not found)\n3. **Failure:** `Effect.fail(new DatabaseError())` (e.g., database connection lost)\n\nBy using `Option` inside the success channel of an `Effect`, you keep the error channel clean for true, unexpected, or unrecoverable errors. The \"not found\" case is often an expected and recoverable part of your business logic, and `Option.none()` models this perfectly.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Option, Data } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\") {}\n\n// This signature is extremely honest about its possible outcomes.\nconst findUserInDb = (\n id: number\n): Effect.Effect, DatabaseError> =>\n Effect.gen(function* () {\n // This could fail with a DatabaseError\n const dbResult = yield* Effect.try({\n try: () => (id === 1 ? { id: 1, name: \"Paul\" } : null),\n catch: () => new DatabaseError(),\n });\n\n // We wrap the potentially null result in an Option\n return Option.fromNullable(dbResult);\n });\n\n// The caller can now handle all three cases explicitly.\nconst program = (id: number) =>\n findUserInDb(id).pipe(\n Effect.flatMap((maybeUser) =>\n Option.match(maybeUser, {\n onNone: () =>\n Effect.logInfo(`Result: User with ID ${id} was not found.`),\n onSome: (user) => Effect.logInfo(`Result: Found user ${user.name}.`),\n })\n ),\n Effect.catchAll((error) =>\n Effect.logInfo(\"Error: Could not connect to the database.\")\n )\n );\n\n// Run the program with different IDs\nEffect.runPromise(\n Effect.gen(function* () {\n // Try with existing user\n yield* Effect.logInfo(\"Looking for user with ID 1...\");\n yield* program(1);\n\n // Try with non-existent user\n yield* Effect.logInfo(\"\\nLooking for user with ID 2...\");\n yield* program(2);\n })\n);\n```\n\n## Anti-Pattern\n\nA common alternative is to create a specific NotFoundError and put it in the error channel alongside other errors.\n\n```typescript\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\") {}\n\n// ❌ This signature conflates two different kinds of failure.\nconst findUserUnsafely = (\n id: number\n): Effect.Effect => {\n // ...\n return Effect.fail(new NotFoundError());\n};\n```\n\nWhile this works, it can be less expressive. It treats a \"not found\" result—which might be a normal part of your application's flow—the same as a catastrophic DatabaseError.\n\nUsing `Effect>` often leads to clearer and more precise business logic." - }, - { - "id": "pattern-matcheffect", - "title": "Effectful Pattern Matching with matchEffect", - "description": "Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Effect: Run different Effects on success or failure\nconst effect = Effect.fail(\"Oops!\").pipe(\n Effect.matchEffect({\n onFailure: (err) => Effect.logError(`Error: ${err}`),\n onSuccess: (value) => Effect.log(`Success: ${value}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `matchEffect` allows you to run an Effect for both the success and failure cases.\n- This is useful for logging, cleanup, retries, or any effectful side effect that depends on the outcome.", - "antiPattern": "Using `match` to return values and then wrapping them in Effects, or duplicating logic for side effects, instead of using `matchEffect` for direct effectful branching.", - "explanation": "Sometimes, handling a success or failure requires running additional Effects (e.g., logging, retries, cleanup). \n`matchEffect` lets you do this declaratively, keeping your code composable and type-safe.", - "content": "# Effectful Pattern Matching with `matchEffect`\n\n## Guideline\n\nUse the `matchEffect` combinator to perform effectful branching based on whether an Effect succeeds or fails. \nThis allows you to run different Effects for each case, enabling rich, composable workflows.\n\n## Rationale\n\nSometimes, handling a success or failure requires running additional Effects (e.g., logging, retries, cleanup). \n`matchEffect` lets you do this declaratively, keeping your code composable and type-safe.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Effect: Run different Effects on success or failure\nconst effect = Effect.fail(\"Oops!\").pipe(\n Effect.matchEffect({\n onFailure: (err) => Effect.logError(`Error: ${err}`),\n onSuccess: (value) => Effect.log(`Success: ${value}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `matchEffect` allows you to run an Effect for both the success and failure cases.\n- This is useful for logging, cleanup, retries, or any effectful side effect that depends on the outcome.\n\n## Anti-Pattern\n\nUsing `match` to return values and then wrapping them in Effects, or duplicating logic for side effects, instead of using `matchEffect` for direct effectful branching." - }, - { - "id": "error-handling-pattern-accumulation", - "title": "Error Handling Pattern 1: Accumulating Multiple Errors", - "description": "Use error accumulation to report all problems at once rather than failing early, critical for validation and batch operations.", - "skillLevel": "intermediate", - "useCases": [ - "error-handling" - ], - "example": "This example demonstrates error accumulation patterns.\n\n```typescript\nimport { Effect, Data, Cause } from \"effect\";\n\ninterface ValidationError {\n field: string;\n message: string;\n value?: unknown;\n}\n\ninterface ProcessingResult {\n successes: T[];\n errors: ValidationError[];\n}\n\n// Example 1: Form validation with error accumulation\nconst program = Effect.gen(function* () {\n console.log(`\\n[ERROR ACCUMULATION] Collecting multiple errors\\n`);\n\n // Form data\n interface FormData {\n name: string;\n email: string;\n age: number;\n phone: string;\n }\n\n const validateForm = (data: FormData): ValidationError[] => {\n const errors: ValidationError[] = [];\n\n // Validation 1: Name\n if (!data.name || data.name.trim().length === 0) {\n errors.push({\n field: \"name\",\n message: \"Name is required\",\n value: data.name,\n });\n } else if (data.name.length < 2) {\n errors.push({\n field: \"name\",\n message: \"Name must be at least 2 characters\",\n value: data.name,\n });\n }\n\n // Validation 2: Email\n if (!data.email) {\n errors.push({\n field: \"email\",\n message: \"Email is required\",\n value: data.email,\n });\n } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(data.email)) {\n errors.push({\n field: \"email\",\n message: \"Email format invalid\",\n value: data.email,\n });\n }\n\n // Validation 3: Age\n if (data.age < 0 || data.age > 150) {\n errors.push({\n field: \"age\",\n message: \"Age must be between 0 and 150\",\n value: data.age,\n });\n }\n\n // Validation 4: Phone\n if (data.phone && !/^\\d{3}-\\d{3}-\\d{4}$/.test(data.phone)) {\n errors.push({\n field: \"phone\",\n message: \"Phone must be in format XXX-XXX-XXXX\",\n value: data.phone,\n });\n }\n\n return errors;\n };\n\n // Example 1: Form with multiple errors\n console.log(`[1] Form validation with multiple errors:\\n`);\n\n const invalidForm: FormData = {\n name: \"\",\n email: \"not-an-email\",\n age: 200,\n phone: \"invalid\",\n };\n\n const validationErrors = validateForm(invalidForm);\n\n yield* Effect.log(`[VALIDATION] Found ${validationErrors.length} errors:\\n`);\n\n for (const error of validationErrors) {\n yield* Effect.log(` ✗ ${error.field}: ${error.message}`);\n }\n\n // Example 2: Batch processing with partial success\n console.log(`\\n[2] Batch processing (accumulate successes and failures):\\n`);\n\n interface Record {\n id: string;\n data: string;\n }\n\n const processRecord = (record: Record): Result => {\n if (record.id.length === 0) {\n return { success: false, error: \"Missing ID\" };\n }\n\n if (record.data.includes(\"ERROR\")) {\n return { success: false, error: \"Invalid data\" };\n }\n\n return { success: true, value: `processed-${record.id}` };\n };\n\n interface Result {\n success: boolean;\n value?: T;\n error?: string;\n }\n\n const records: Record[] = [\n { id: \"rec1\", data: \"ok\" },\n { id: \"\", data: \"ok\" }, // Error: missing ID\n { id: \"rec3\", data: \"ok\" },\n { id: \"rec4\", data: \"ERROR\" }, // Error: invalid data\n { id: \"rec5\", data: \"ok\" },\n ];\n\n const results: ProcessingResult = {\n successes: [],\n errors: [],\n };\n\n for (const record of records) {\n const result = processRecord(record);\n\n if (result.success) {\n results.successes.push(result.value!);\n } else {\n results.errors.push({\n field: record.id || \"unknown\",\n message: result.error!,\n });\n }\n }\n\n yield* Effect.log(\n `[BATCH] Processed ${records.length} records`\n );\n yield* Effect.log(`[BATCH] ✓ ${results.successes.length} succeeded`);\n yield* Effect.log(`[BATCH] ✗ ${results.errors.length} failed\\n`);\n\n for (const success of results.successes) {\n yield* Effect.log(` ✓ ${success}`);\n }\n\n for (const error of results.errors) {\n yield* Effect.log(` ✗ [${error.field}] ${error.message}`);\n }\n\n // Example 3: Multi-step validation with error accumulation\n console.log(`\\n[3] Multi-step validation (all checks run):\\n`);\n\n interface ServiceHealth {\n diskSpace: boolean;\n memory: boolean;\n network: boolean;\n database: boolean;\n }\n\n const diagnostics: ValidationError[] = [];\n\n // Check 1: Disk space\n const diskFree = 50; // MB\n\n if (diskFree < 100) {\n diagnostics.push({\n field: \"disk-space\",\n message: `Only ${diskFree}MB free (need 100MB)`,\n value: diskFree,\n });\n }\n\n // Check 2: Memory\n const memUsage = 95; // percent\n\n if (memUsage > 85) {\n diagnostics.push({\n field: \"memory\",\n message: `Using ${memUsage}% (threshold: 85%)`,\n value: memUsage,\n });\n }\n\n // Check 3: Network\n const latency = 500; // ms\n\n if (latency > 200) {\n diagnostics.push({\n field: \"network\",\n message: `Latency ${latency}ms (threshold: 200ms)`,\n value: latency,\n });\n }\n\n // Check 4: Database\n const dbConnections = 95;\n const dbMax = 100;\n\n if (dbConnections > dbMax * 0.8) {\n diagnostics.push({\n field: \"database\",\n message: `${dbConnections}/${dbMax} connections (80% threshold)`,\n value: dbConnections,\n });\n }\n\n if (diagnostics.length === 0) {\n yield* Effect.log(`[HEALTH] ✓ All systems normal\\n`);\n } else {\n yield* Effect.log(\n `[HEALTH] ✗ ${diagnostics.length} issue(s) detected:\\n`\n );\n\n for (const diag of diagnostics) {\n yield* Effect.log(` ⚠ ${diag.field}: ${diag.message}`);\n }\n }\n\n // Example 4: Error collection with retry decisions\n console.log(`\\n[4] Error collection for retry strategy:\\n`);\n\n interface ErrorWithContext {\n operation: string;\n error: string;\n retryable: boolean;\n timestamp: Date;\n }\n\n const operationErrors: ErrorWithContext[] = [];\n\n const operations = [\n { name: \"fetch-config\", fail: false },\n { name: \"connect-db\", fail: true },\n { name: \"load-cache\", fail: true },\n { name: \"start-server\", fail: false },\n ];\n\n for (const op of operations) {\n if (op.fail) {\n operationErrors.push({\n operation: op.name,\n error: \"Operation failed\",\n retryable: op.name !== \"fetch-config\",\n timestamp: new Date(),\n });\n }\n }\n\n yield* Effect.log(`[OPERATIONS] ${operationErrors.length} errors:\\n`);\n\n for (const err of operationErrors) {\n const status = err.retryable ? \"🔄 retryable\" : \"❌ non-retryable\";\n yield* Effect.log(` ${status}: ${err.operation}`);\n }\n\n if (operationErrors.every((e) => e.retryable)) {\n yield* Effect.log(`\\n[DECISION] All errors retryable, will retry\\n`);\n } else {\n yield* Effect.log(`\\n[DECISION] Some non-retryable errors, manual intervention needed\\n`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Failing fast causes problems:\n\n**Problem 1: Form validation**\n- User submits form with 10 field errors\n- Fail on first error: \"Name required\"\n- User fixes name, submits again\n- New error: \"Email invalid\"\n- User submits 10 times before fixing all errors\n- Frustration, reduced productivity\n\n**Problem 2: Batch processing**\n- Process 1000 records, fail on record 5\n- 995 records not processed\n- User manually retries\n- Repeats for each error type\n- Inefficient\n\n**Problem 3: System diagnostics**\n- Service health check fails\n- Report: \"Check 1 failed\"\n- Fix check 1, service still down\n- Hidden problem: checks 2, 3, and 4 also failed\n- Time wasted diagnosing\n\nSolutions:\n\n**Error accumulation**:\n- Run all validations\n- Collect errors\n- Report all problems\n- User fixes once, not 10 times\n\n**Partial success**:\n- Process all records\n- Track successes and failures\n- Return: \"950 succeeded, 50 failed\"\n- No re-processing\n\n**Comprehensive diagnostics**:\n- Run all checks\n- Report all failures\n- Quick root cause analysis\n- Faster resolution\n\n---", - "content": "## Guideline\n\nError accumulation strategies:\n\n- **Collect errors**: Gather all failures before reporting\n- **Fail late**: Continue processing despite errors\n- **Contextual errors**: Keep error location/operation info\n- **Error summary**: Aggregate for reporting\n- **Partial success**: Return valid results + errors\n\nPattern: Use `Cause` aggregation, `Result` types, or custom error structures\n\n---\n\n## Rationale\n\nFailing fast causes problems:\n\n**Problem 1: Form validation**\n- User submits form with 10 field errors\n- Fail on first error: \"Name required\"\n- User fixes name, submits again\n- New error: \"Email invalid\"\n- User submits 10 times before fixing all errors\n- Frustration, reduced productivity\n\n**Problem 2: Batch processing**\n- Process 1000 records, fail on record 5\n- 995 records not processed\n- User manually retries\n- Repeats for each error type\n- Inefficient\n\n**Problem 3: System diagnostics**\n- Service health check fails\n- Report: \"Check 1 failed\"\n- Fix check 1, service still down\n- Hidden problem: checks 2, 3, and 4 also failed\n- Time wasted diagnosing\n\nSolutions:\n\n**Error accumulation**:\n- Run all validations\n- Collect errors\n- Report all problems\n- User fixes once, not 10 times\n\n**Partial success**:\n- Process all records\n- Track successes and failures\n- Return: \"950 succeeded, 50 failed\"\n- No re-processing\n\n**Comprehensive diagnostics**:\n- Run all checks\n- Report all failures\n- Quick root cause analysis\n- Faster resolution\n\n---\n\n## Good Example\n\nThis example demonstrates error accumulation patterns.\n\n```typescript\nimport { Effect, Data, Cause } from \"effect\";\n\ninterface ValidationError {\n field: string;\n message: string;\n value?: unknown;\n}\n\ninterface ProcessingResult {\n successes: T[];\n errors: ValidationError[];\n}\n\n// Example 1: Form validation with error accumulation\nconst program = Effect.gen(function* () {\n console.log(`\\n[ERROR ACCUMULATION] Collecting multiple errors\\n`);\n\n // Form data\n interface FormData {\n name: string;\n email: string;\n age: number;\n phone: string;\n }\n\n const validateForm = (data: FormData): ValidationError[] => {\n const errors: ValidationError[] = [];\n\n // Validation 1: Name\n if (!data.name || data.name.trim().length === 0) {\n errors.push({\n field: \"name\",\n message: \"Name is required\",\n value: data.name,\n });\n } else if (data.name.length < 2) {\n errors.push({\n field: \"name\",\n message: \"Name must be at least 2 characters\",\n value: data.name,\n });\n }\n\n // Validation 2: Email\n if (!data.email) {\n errors.push({\n field: \"email\",\n message: \"Email is required\",\n value: data.email,\n });\n } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(data.email)) {\n errors.push({\n field: \"email\",\n message: \"Email format invalid\",\n value: data.email,\n });\n }\n\n // Validation 3: Age\n if (data.age < 0 || data.age > 150) {\n errors.push({\n field: \"age\",\n message: \"Age must be between 0 and 150\",\n value: data.age,\n });\n }\n\n // Validation 4: Phone\n if (data.phone && !/^\\d{3}-\\d{3}-\\d{4}$/.test(data.phone)) {\n errors.push({\n field: \"phone\",\n message: \"Phone must be in format XXX-XXX-XXXX\",\n value: data.phone,\n });\n }\n\n return errors;\n };\n\n // Example 1: Form with multiple errors\n console.log(`[1] Form validation with multiple errors:\\n`);\n\n const invalidForm: FormData = {\n name: \"\",\n email: \"not-an-email\",\n age: 200,\n phone: \"invalid\",\n };\n\n const validationErrors = validateForm(invalidForm);\n\n yield* Effect.log(`[VALIDATION] Found ${validationErrors.length} errors:\\n`);\n\n for (const error of validationErrors) {\n yield* Effect.log(` ✗ ${error.field}: ${error.message}`);\n }\n\n // Example 2: Batch processing with partial success\n console.log(`\\n[2] Batch processing (accumulate successes and failures):\\n`);\n\n interface Record {\n id: string;\n data: string;\n }\n\n const processRecord = (record: Record): Result => {\n if (record.id.length === 0) {\n return { success: false, error: \"Missing ID\" };\n }\n\n if (record.data.includes(\"ERROR\")) {\n return { success: false, error: \"Invalid data\" };\n }\n\n return { success: true, value: `processed-${record.id}` };\n };\n\n interface Result {\n success: boolean;\n value?: T;\n error?: string;\n }\n\n const records: Record[] = [\n { id: \"rec1\", data: \"ok\" },\n { id: \"\", data: \"ok\" }, // Error: missing ID\n { id: \"rec3\", data: \"ok\" },\n { id: \"rec4\", data: \"ERROR\" }, // Error: invalid data\n { id: \"rec5\", data: \"ok\" },\n ];\n\n const results: ProcessingResult = {\n successes: [],\n errors: [],\n };\n\n for (const record of records) {\n const result = processRecord(record);\n\n if (result.success) {\n results.successes.push(result.value!);\n } else {\n results.errors.push({\n field: record.id || \"unknown\",\n message: result.error!,\n });\n }\n }\n\n yield* Effect.log(\n `[BATCH] Processed ${records.length} records`\n );\n yield* Effect.log(`[BATCH] ✓ ${results.successes.length} succeeded`);\n yield* Effect.log(`[BATCH] ✗ ${results.errors.length} failed\\n`);\n\n for (const success of results.successes) {\n yield* Effect.log(` ✓ ${success}`);\n }\n\n for (const error of results.errors) {\n yield* Effect.log(` ✗ [${error.field}] ${error.message}`);\n }\n\n // Example 3: Multi-step validation with error accumulation\n console.log(`\\n[3] Multi-step validation (all checks run):\\n`);\n\n interface ServiceHealth {\n diskSpace: boolean;\n memory: boolean;\n network: boolean;\n database: boolean;\n }\n\n const diagnostics: ValidationError[] = [];\n\n // Check 1: Disk space\n const diskFree = 50; // MB\n\n if (diskFree < 100) {\n diagnostics.push({\n field: \"disk-space\",\n message: `Only ${diskFree}MB free (need 100MB)`,\n value: diskFree,\n });\n }\n\n // Check 2: Memory\n const memUsage = 95; // percent\n\n if (memUsage > 85) {\n diagnostics.push({\n field: \"memory\",\n message: `Using ${memUsage}% (threshold: 85%)`,\n value: memUsage,\n });\n }\n\n // Check 3: Network\n const latency = 500; // ms\n\n if (latency > 200) {\n diagnostics.push({\n field: \"network\",\n message: `Latency ${latency}ms (threshold: 200ms)`,\n value: latency,\n });\n }\n\n // Check 4: Database\n const dbConnections = 95;\n const dbMax = 100;\n\n if (dbConnections > dbMax * 0.8) {\n diagnostics.push({\n field: \"database\",\n message: `${dbConnections}/${dbMax} connections (80% threshold)`,\n value: dbConnections,\n });\n }\n\n if (diagnostics.length === 0) {\n yield* Effect.log(`[HEALTH] ✓ All systems normal\\n`);\n } else {\n yield* Effect.log(\n `[HEALTH] ✗ ${diagnostics.length} issue(s) detected:\\n`\n );\n\n for (const diag of diagnostics) {\n yield* Effect.log(` ⚠ ${diag.field}: ${diag.message}`);\n }\n }\n\n // Example 4: Error collection with retry decisions\n console.log(`\\n[4] Error collection for retry strategy:\\n`);\n\n interface ErrorWithContext {\n operation: string;\n error: string;\n retryable: boolean;\n timestamp: Date;\n }\n\n const operationErrors: ErrorWithContext[] = [];\n\n const operations = [\n { name: \"fetch-config\", fail: false },\n { name: \"connect-db\", fail: true },\n { name: \"load-cache\", fail: true },\n { name: \"start-server\", fail: false },\n ];\n\n for (const op of operations) {\n if (op.fail) {\n operationErrors.push({\n operation: op.name,\n error: \"Operation failed\",\n retryable: op.name !== \"fetch-config\",\n timestamp: new Date(),\n });\n }\n }\n\n yield* Effect.log(`[OPERATIONS] ${operationErrors.length} errors:\\n`);\n\n for (const err of operationErrors) {\n const status = err.retryable ? \"🔄 retryable\" : \"❌ non-retryable\";\n yield* Effect.log(` ${status}: ${err.operation}`);\n }\n\n if (operationErrors.every((e) => e.retryable)) {\n yield* Effect.log(`\\n[DECISION] All errors retryable, will retry\\n`);\n } else {\n yield* Effect.log(`\\n[DECISION] Some non-retryable errors, manual intervention needed\\n`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Validation Schema with Error Accumulation\n\nBuild type-safe validation:\n\n```typescript\ninterface ValidatedResult {\n tag: \"success\" | \"failure\";\n value?: T;\n errors?: ValidationError[];\n}\n\nconst validateUser = (data: Record): ValidatedResult<{\n name: string;\n email: string;\n}> => {\n const errors: ValidationError[] = [];\n\n // All validations run\n const name = String(data.name ?? \"\");\n\n if (!name || name.trim().length === 0) {\n errors.push({\n field: \"name\",\n message: \"Name required\",\n });\n }\n\n const email = String(data.email ?? \"\");\n\n if (!email.includes(\"@\")) {\n errors.push({\n field: \"email\",\n message: \"Invalid email\",\n });\n }\n\n if (errors.length > 0) {\n return { tag: \"failure\", errors };\n }\n\n return {\n tag: \"success\",\n value: { name, email },\n };\n};\n```\n\n---\n\n## Advanced: Cause Aggregation\n\nUse Effect's Cause for error tracking:\n\n```typescript\nconst aggregateErrors = (effects: Array>) =>\n Effect.gen(function* () {\n const results = yield* Effect.forEach(\n effects,\n (effect) =>\n effect.pipe(\n Effect.mapError((error) => [error]), // Wrap in array\n Effect.asVoid // Discard value\n ),\n { discard: false } // Collect all results\n ).pipe(\n Effect.catchAll((causes) =>\n // causes contains all accumulated errors\n Effect.gen(function* () {\n yield* Effect.log(`Collected ${causes.length} errors`);\n return causes;\n })\n )\n );\n\n return results;\n });\n```\n\n---\n\n## Advanced: Streaming Errors\n\nAccumulate errors over streams:\n\n```typescript\nconst streamWithErrorCollection = (\n source: Stream.Stream\n) =>\n Effect.gen(function* () {\n const errors = yield* Ref.make([]);\n\n const processed = yield* source.pipe(\n Stream.tap((item) =>\n // Validate each item\n validateItem(item).pipe(\n Effect.mapError((error) =>\n Ref.update(errors, (list) => [...list, error])\n ),\n Effect.asVoid\n )\n ),\n Stream.runDrain\n );\n\n const collectedErrors = yield* Ref.get(errors);\n\n return { processed, errors: collectedErrors };\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use error accumulation when:**\n- Form validation\n- Batch processing\n- Configuration validation\n- Health checks\n- Multi-step initialization\n\n⚠️ **Don't use when:**\n- Critical failure (must stop immediately)\n- Recovery depends on single error\n- Error interdependencies matter\n\n---\n\n## Error Accumulation Strategies\n\n| Strategy | When | Trade-off |\n| --- | --- | --- |\n| **Fail fast** | Critical errors | Poor UX, rework |\n| **Accumulate all** | Validation | Harder to prioritize |\n| **Accumulate + tier** | Mixed severity | More complex logic |\n| **Sampling** | Large batches | Miss some errors |\n\n---\n\n## See Also\n\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error chains\n- [Error Handling Pattern 3: Custom Strategies](./error-handling-pattern-custom-strategies.mdx) - Custom errors\n- [Stream Pattern 4: Stateful Operations](./stream-pattern-stateful-operations.mdx) - Accumulation patterns\n- [Scheduling Pattern 5: Advanced Retries](./scheduling-pattern-advanced-retry-chains.mdx) - Error classification" - }, - { - "id": "error-handling-pattern-propagation", - "title": "Error Handling Pattern 2: Error Propagation and Chains", - "description": "Use error propagation to preserve context through effect chains, enabling debugging and recovery at the right abstraction level.", - "skillLevel": "advanced", - "useCases": [ - "error-handling" - ], - "example": "This example demonstrates error propagation with context.\n\n```typescript\nimport { Effect, Data, Cause } from \"effect\";\n\n// Domain-specific errors with context\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n query: string;\n parameters: unknown[];\n cause: Error;\n}> {}\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n endpoint: string;\n method: string;\n statusCode?: number;\n cause: Error;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n field: string;\n value: unknown;\n reason: string;\n}> {}\n\nclass BusinessLogicError extends Data.TaggedError(\"BusinessLogicError\")<{\n operation: string;\n context: Record;\n originalError: Error;\n}> {}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ERROR PROPAGATION] Error chains with context\\n`);\n\n // Example 1: Simple error propagation\n console.log(`[1] Error propagation through layers:\\n`);\n\n const lowLevelOperation = Effect.gen(function* () {\n yield* Effect.log(`[LAYER 1] Low-level operation starting`);\n\n yield* Effect.fail(new Error(\"File not found\"));\n });\n\n const midLevelOperation = lowLevelOperation.pipe(\n Effect.mapError((error) =>\n new DatabaseError({\n query: \"SELECT * FROM users\",\n parameters: [\"id=123\"],\n cause: error instanceof Error ? error : new Error(String(error)),\n })\n )\n );\n\n const highLevelOperation = midLevelOperation.pipe(\n Effect.catchTag(\"DatabaseError\", (dbError) =>\n Effect.gen(function* () {\n yield* Effect.log(`[LAYER 3] Caught database error`);\n yield* Effect.log(`[LAYER 3] Query: ${dbError.query}`);\n yield* Effect.log(`[LAYER 3] Cause: ${dbError.cause.message}`);\n\n // Recovery decision\n return \"fallback-value\";\n })\n )\n );\n\n const result1 = yield* highLevelOperation;\n\n yield* Effect.log(`[RESULT] Recovered with: ${result1}\\n`);\n\n // Example 2: Error context accumulation\n console.log(`[2] Accumulating context through layers:\\n`);\n\n interface ErrorContext {\n timestamp: Date;\n operation: string;\n userId?: string;\n requestId: string;\n }\n\n const errorWithContext = (context: ErrorContext) =>\n Effect.fail(\n new BusinessLogicError({\n operation: context.operation,\n context: {\n userId: context.userId,\n timestamp: context.timestamp.toISOString(),\n requestId: context.requestId,\n },\n originalError: new Error(\"Operation failed\"),\n })\n );\n\n const myContext: ErrorContext = {\n timestamp: new Date(),\n operation: \"process-payment\",\n userId: \"user-123\",\n requestId: \"req-abc-def\",\n };\n\n const withContextRecovery = errorWithContext(myContext).pipe(\n Effect.mapError((error) => {\n // Log complete context\n return {\n ...error,\n enriched: true,\n additionalInfo: {\n serviceName: \"payment-service\",\n environment: \"production\",\n version: \"1.2.3\",\n },\n };\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR CAUGHT] ${error.operation}`);\n yield* Effect.log(`[CONTEXT] ${JSON.stringify(error.context, null, 2)}`);\n return \"recovered\";\n })\n )\n );\n\n yield* withContextRecovery;\n\n // Example 3: Network error with retry context\n console.log(`\\n[3] Network errors with retry context:\\n`);\n\n interface RetryContext {\n attempt: number;\n maxAttempts: number;\n delay: number;\n }\n\n let attemptCount = 0;\n\n const networkCall = Effect.gen(function* () {\n attemptCount++;\n\n yield* Effect.log(`[ATTEMPT] ${attemptCount}/3`);\n\n if (attemptCount < 3) {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"https://api.example.com/data\",\n method: \"GET\",\n statusCode: 503,\n cause: new Error(\"Service Unavailable\"),\n })\n );\n }\n\n return \"success\";\n });\n\n const withRetryContext = Effect.gen(function* () {\n let lastError: NetworkError | null = null;\n\n for (let i = 1; i <= 3; i++) {\n const result = yield* networkCall.pipe(\n Effect.catchTag(\"NetworkError\", (error) => {\n lastError = error;\n\n yield* Effect.log(\n `[RETRY] Attempt ${i} failed: ${error.statusCode}`\n );\n\n if (i < 3) {\n yield* Effect.log(`[RETRY] Waiting before retry...`);\n }\n\n return Effect.fail(error);\n })\n ).pipe(\n Effect.tap(() => Effect.log(`[SUCCESS] Connected on attempt ${i}`))\n ).pipe(\n Effect.catchAll(() => Effect.succeed(null))\n );\n\n if (result !== null) {\n return result;\n }\n }\n\n if (lastError) {\n yield* Effect.fail(lastError);\n }\n\n return null;\n });\n\n const networkResult = yield* withRetryContext.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[EXHAUSTED] All retries failed`);\n return \"fallback\";\n })\n )\n );\n\n yield* Effect.log(`\\n`);\n\n // Example 4: Multi-layer error transformation\n console.log(`[4] Error transformation between layers:\\n`);\n\n const layer1Error = Effect.gen(function* () {\n yield* Effect.fail(new Error(\"Raw system error\"));\n });\n\n // Layer 2: Convert to domain error\n const layer2 = layer1Error.pipe(\n Effect.mapError((error) =>\n new DatabaseError({\n query: \"SELECT ...\",\n parameters: [],\n cause: error instanceof Error ? error : new Error(String(error)),\n })\n )\n );\n\n // Layer 3: Convert to business error\n const layer3 = layer2.pipe(\n Effect.mapError((dbError) =>\n new BusinessLogicError({\n operation: \"fetch-user-profile\",\n context: {\n dbError: dbError.query,\n },\n originalError: dbError.cause,\n })\n )\n );\n\n // Layer 4: Return user-friendly error\n const userFacingError = layer3.pipe(\n Effect.mapError((bizError) => ({\n message: \"Unable to load profile\",\n code: \"PROFILE_LOAD_FAILED\",\n originalError: bizError.originalError.message,\n })),\n Effect.catchAll((userError) =>\n Effect.gen(function* () {\n yield* Effect.log(`[USER MESSAGE] ${userError.message}`);\n yield* Effect.log(`[CODE] ${userError.code}`);\n yield* Effect.log(`[DEBUG] ${userError.originalError}`);\n return null;\n })\n )\n );\n\n yield* userFacingError;\n\n // Example 5: Error aggregation in concurrent operations\n console.log(`\\n[5] Error propagation in concurrent operations:\\n`);\n\n const operation = (id: number, shouldFail: boolean) =>\n Effect.gen(function* () {\n if (shouldFail) {\n yield* Effect.fail(\n new Error(`Operation ${id} failed`)\n );\n }\n\n return `result-${id}`;\n });\n\n const concurrent = Effect.gen(function* () {\n const results = yield* Effect.all(\n [\n operation(1, false),\n operation(2, true),\n operation(3, false),\n ],\n { concurrency: 3 }\n ).pipe(\n Effect.catchAll((errors) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONCURRENT] Caught aggregated errors`);\n\n // In real code, Cause provides error details\n yield* Effect.log(`[ERROR] Errors encountered during concurrent execution`);\n\n return [];\n })\n )\n );\n\n return results;\n });\n\n yield* concurrent;\n\n yield* Effect.log(`\\n[DEMO] Error propagation complete`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Loss of error context causes problems:\n\n**Problem 1: Useless error messages**\n- User sees: \"Error: null\"\n- Debugging: Where did it come from? When? Why?\n- Wasted hours searching logs\n\n**Problem 2: Wrong recovery layer**\n- Network error → recovered at business logic layer (inefficient)\n- Should be recovered at network layer → retry, exponential backoff\n\n**Problem 3: Error context loss**\n- Database connection failed\n- But which database? Which query? With what parameters?\n- Logs show \"Connection failed\" (not actionable)\n\n**Problem 4: Hidden root cause**\n- Effect 1 fails → triggers Effect 2 → different error\n- Developer sees Effect 2 error\n- Doesn't know Effect 1 was root cause\n- Fixes wrong thing\n\nSolutions:\n\n**Error context**:\n- Include operation name\n- Include relevant parameters\n- Include timestamps\n- Include retry count\n\n**Error cause chains**:\n- Keep original error\n- Add context at each layer\n- `mapError()` to transform\n- `tapError()` to log context\n\n**Recovery layers**:\n- Low-level: Retry network requests\n- Mid-level: Transform domain errors\n- High-level: Convert to user-friendly messages\n\n---", - "content": "## Guideline\n\nError propagation preserves context:\n\n- **Cause chain**: Keep original error + context\n- **Stack trace**: Preserve execution history\n- **Error context**: Add operation name, parameters\n- **Error mapping**: Transform errors between layers\n- **Recovery points**: Decide where to handle errors\n\nPattern: Use `mapError()`, `tapError()`, `catchAll()`, `Cause.prettyPrint()`\n\n---\n\n## Rationale\n\nLoss of error context causes problems:\n\n**Problem 1: Useless error messages**\n- User sees: \"Error: null\"\n- Debugging: Where did it come from? When? Why?\n- Wasted hours searching logs\n\n**Problem 2: Wrong recovery layer**\n- Network error → recovered at business logic layer (inefficient)\n- Should be recovered at network layer → retry, exponential backoff\n\n**Problem 3: Error context loss**\n- Database connection failed\n- But which database? Which query? With what parameters?\n- Logs show \"Connection failed\" (not actionable)\n\n**Problem 4: Hidden root cause**\n- Effect 1 fails → triggers Effect 2 → different error\n- Developer sees Effect 2 error\n- Doesn't know Effect 1 was root cause\n- Fixes wrong thing\n\nSolutions:\n\n**Error context**:\n- Include operation name\n- Include relevant parameters\n- Include timestamps\n- Include retry count\n\n**Error cause chains**:\n- Keep original error\n- Add context at each layer\n- `mapError()` to transform\n- `tapError()` to log context\n\n**Recovery layers**:\n- Low-level: Retry network requests\n- Mid-level: Transform domain errors\n- High-level: Convert to user-friendly messages\n\n---\n\n## Good Example\n\nThis example demonstrates error propagation with context.\n\n```typescript\nimport { Effect, Data, Cause } from \"effect\";\n\n// Domain-specific errors with context\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n query: string;\n parameters: unknown[];\n cause: Error;\n}> {}\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n endpoint: string;\n method: string;\n statusCode?: number;\n cause: Error;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n field: string;\n value: unknown;\n reason: string;\n}> {}\n\nclass BusinessLogicError extends Data.TaggedError(\"BusinessLogicError\")<{\n operation: string;\n context: Record;\n originalError: Error;\n}> {}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ERROR PROPAGATION] Error chains with context\\n`);\n\n // Example 1: Simple error propagation\n console.log(`[1] Error propagation through layers:\\n`);\n\n const lowLevelOperation = Effect.gen(function* () {\n yield* Effect.log(`[LAYER 1] Low-level operation starting`);\n\n yield* Effect.fail(new Error(\"File not found\"));\n });\n\n const midLevelOperation = lowLevelOperation.pipe(\n Effect.mapError((error) =>\n new DatabaseError({\n query: \"SELECT * FROM users\",\n parameters: [\"id=123\"],\n cause: error instanceof Error ? error : new Error(String(error)),\n })\n )\n );\n\n const highLevelOperation = midLevelOperation.pipe(\n Effect.catchTag(\"DatabaseError\", (dbError) =>\n Effect.gen(function* () {\n yield* Effect.log(`[LAYER 3] Caught database error`);\n yield* Effect.log(`[LAYER 3] Query: ${dbError.query}`);\n yield* Effect.log(`[LAYER 3] Cause: ${dbError.cause.message}`);\n\n // Recovery decision\n return \"fallback-value\";\n })\n )\n );\n\n const result1 = yield* highLevelOperation;\n\n yield* Effect.log(`[RESULT] Recovered with: ${result1}\\n`);\n\n // Example 2: Error context accumulation\n console.log(`[2] Accumulating context through layers:\\n`);\n\n interface ErrorContext {\n timestamp: Date;\n operation: string;\n userId?: string;\n requestId: string;\n }\n\n const errorWithContext = (context: ErrorContext) =>\n Effect.fail(\n new BusinessLogicError({\n operation: context.operation,\n context: {\n userId: context.userId,\n timestamp: context.timestamp.toISOString(),\n requestId: context.requestId,\n },\n originalError: new Error(\"Operation failed\"),\n })\n );\n\n const myContext: ErrorContext = {\n timestamp: new Date(),\n operation: \"process-payment\",\n userId: \"user-123\",\n requestId: \"req-abc-def\",\n };\n\n const withContextRecovery = errorWithContext(myContext).pipe(\n Effect.mapError((error) => {\n // Log complete context\n return {\n ...error,\n enriched: true,\n additionalInfo: {\n serviceName: \"payment-service\",\n environment: \"production\",\n version: \"1.2.3\",\n },\n };\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR CAUGHT] ${error.operation}`);\n yield* Effect.log(`[CONTEXT] ${JSON.stringify(error.context, null, 2)}`);\n return \"recovered\";\n })\n )\n );\n\n yield* withContextRecovery;\n\n // Example 3: Network error with retry context\n console.log(`\\n[3] Network errors with retry context:\\n`);\n\n interface RetryContext {\n attempt: number;\n maxAttempts: number;\n delay: number;\n }\n\n let attemptCount = 0;\n\n const networkCall = Effect.gen(function* () {\n attemptCount++;\n\n yield* Effect.log(`[ATTEMPT] ${attemptCount}/3`);\n\n if (attemptCount < 3) {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"https://api.example.com/data\",\n method: \"GET\",\n statusCode: 503,\n cause: new Error(\"Service Unavailable\"),\n })\n );\n }\n\n return \"success\";\n });\n\n const withRetryContext = Effect.gen(function* () {\n let lastError: NetworkError | null = null;\n\n for (let i = 1; i <= 3; i++) {\n const result = yield* networkCall.pipe(\n Effect.catchTag(\"NetworkError\", (error) => {\n lastError = error;\n\n yield* Effect.log(\n `[RETRY] Attempt ${i} failed: ${error.statusCode}`\n );\n\n if (i < 3) {\n yield* Effect.log(`[RETRY] Waiting before retry...`);\n }\n\n return Effect.fail(error);\n })\n ).pipe(\n Effect.tap(() => Effect.log(`[SUCCESS] Connected on attempt ${i}`))\n ).pipe(\n Effect.catchAll(() => Effect.succeed(null))\n );\n\n if (result !== null) {\n return result;\n }\n }\n\n if (lastError) {\n yield* Effect.fail(lastError);\n }\n\n return null;\n });\n\n const networkResult = yield* withRetryContext.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[EXHAUSTED] All retries failed`);\n return \"fallback\";\n })\n )\n );\n\n yield* Effect.log(`\\n`);\n\n // Example 4: Multi-layer error transformation\n console.log(`[4] Error transformation between layers:\\n`);\n\n const layer1Error = Effect.gen(function* () {\n yield* Effect.fail(new Error(\"Raw system error\"));\n });\n\n // Layer 2: Convert to domain error\n const layer2 = layer1Error.pipe(\n Effect.mapError((error) =>\n new DatabaseError({\n query: \"SELECT ...\",\n parameters: [],\n cause: error instanceof Error ? error : new Error(String(error)),\n })\n )\n );\n\n // Layer 3: Convert to business error\n const layer3 = layer2.pipe(\n Effect.mapError((dbError) =>\n new BusinessLogicError({\n operation: \"fetch-user-profile\",\n context: {\n dbError: dbError.query,\n },\n originalError: dbError.cause,\n })\n )\n );\n\n // Layer 4: Return user-friendly error\n const userFacingError = layer3.pipe(\n Effect.mapError((bizError) => ({\n message: \"Unable to load profile\",\n code: \"PROFILE_LOAD_FAILED\",\n originalError: bizError.originalError.message,\n })),\n Effect.catchAll((userError) =>\n Effect.gen(function* () {\n yield* Effect.log(`[USER MESSAGE] ${userError.message}`);\n yield* Effect.log(`[CODE] ${userError.code}`);\n yield* Effect.log(`[DEBUG] ${userError.originalError}`);\n return null;\n })\n )\n );\n\n yield* userFacingError;\n\n // Example 5: Error aggregation in concurrent operations\n console.log(`\\n[5] Error propagation in concurrent operations:\\n`);\n\n const operation = (id: number, shouldFail: boolean) =>\n Effect.gen(function* () {\n if (shouldFail) {\n yield* Effect.fail(\n new Error(`Operation ${id} failed`)\n );\n }\n\n return `result-${id}`;\n });\n\n const concurrent = Effect.gen(function* () {\n const results = yield* Effect.all(\n [\n operation(1, false),\n operation(2, true),\n operation(3, false),\n ],\n { concurrency: 3 }\n ).pipe(\n Effect.catchAll((errors) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONCURRENT] Caught aggregated errors`);\n\n // In real code, Cause provides error details\n yield* Effect.log(`[ERROR] Errors encountered during concurrent execution`);\n\n return [];\n })\n )\n );\n\n return results;\n });\n\n yield* concurrent;\n\n yield* Effect.log(`\\n[DEMO] Error propagation complete`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Cause Chain Introspection\n\nInspect error causes for debugging:\n\n```typescript\nconst inspectCauseChain = (effect: Effect.Effect) =>\n Effect.gen(function* () {\n const result = yield* effect.pipe(\n Effect.mapError((error) => {\n // In real code, inspect cause chain\n if (error instanceof Error) {\n return {\n message: error.message,\n stack: error.stack,\n cause: (error as any).cause,\n };\n }\n\n return error;\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[CAUSE] ${JSON.stringify(error, null, 2)}`\n );\n return null;\n })\n )\n );\n\n return result;\n });\n```\n\n---\n\n## Advanced: Hierarchical Error Recovery\n\nRecover at appropriate layers:\n\n```typescript\ninterface RecoveryStrategy {\n canRecover: (error: Error) => boolean;\n recover: (error: Error) => Effect.Effect;\n level: \"low\" | \"mid\" | \"high\";\n}\n\nconst applyRecoveryStrategies = (\n effect: Effect.Effect,\n strategies: RecoveryStrategy[]\n) => {\n const sorted = strategies.sort((a, b) => {\n const levelOrder = { low: 0, mid: 1, high: 2 };\n return levelOrder[a.level] - levelOrder[b.level];\n });\n\n return sorted.reduce(\n (current, strategy) =>\n current.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (strategy.canRecover(error as Error)) {\n yield* Effect.log(\n `[RECOVER] Using ${strategy.level}-level strategy`\n );\n\n return yield* strategy.recover(error as Error);\n }\n\n return yield* Effect.fail(error);\n })\n )\n ),\n effect\n );\n};\n```\n\n---\n\n## Advanced: Error Telemetry\n\nTrack error propagation for observability:\n\n```typescript\ninterface ErrorTelemetry {\n errorType: string;\n message: string;\n layers: string[];\n timestamp: Date;\n duration: number;\n}\n\nconst trackErrorPropagation = (\n effect: Effect.Effect,\n operationName: string\n) =>\n Effect.gen(function* () {\n const startTime = Date.now();\n const layers: string[] = [];\n\n const tracked = effect.pipe(\n Effect.mapError((error) => {\n layers.push(\"layer-1\");\n return error;\n }),\n Effect.mapError((error) => {\n layers.push(\"layer-2\");\n return error;\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n const duration = Date.now() - startTime;\n\n const telemetry: ErrorTelemetry = {\n errorType: error instanceof Error\n ? error.constructor.name\n : \"Unknown\",\n message: error instanceof Error ? error.message : String(error),\n layers,\n timestamp: new Date(),\n duration,\n };\n\n yield* Effect.log(\n `[TELEMETRY] ${JSON.stringify(telemetry)}`\n );\n\n return null;\n })\n )\n );\n\n return tracked;\n });\n```\n\n---\n\n## Error Context Best Practices\n\n| Practice | Benefit | Trade-off |\n| --- | --- | --- |\n| **Add timestamps** | Correlate logs | Slight overhead |\n| **Include request ID** | Trace across services | Requires coordination |\n| **Track attempt count** | Retry analysis | More state |\n| **Include parameters** | Reproduce issue | Privacy concerns |\n| **Stack traces** | Root cause | Performance cost |\n\n---\n\n## When to Use This Pattern\n\n✅ **Use error propagation when:**\n- Debugging production issues\n- Multi-layer architectures\n- Distributed systems\n- Complex error recovery\n- Audit requirements\n\n✅ **Add context when:**\n- Error affects multiple layers\n- Need reproduce information\n- Audit trail required\n- Performance analysis needed\n\n⚠️ **Trade-offs:**\n- More memory for error objects\n- Serialization overhead\n- Privacy concerns (parameters)\n- Performance impact\n\n---\n\n## Error Recovery Decision Tree\n\n```\nError occurs\n ↓\nIs it retryable?\n ├─ Yes → Can recover here?\n │ ├─ Yes → Recover & continue\n │ └─ No → Propagate up\n └─ No → Propagate up with context\n ↓\n Next layer catches\n ↓\n Can recover?\n ├─ Yes → Recover & continue\n └─ No → Propagate up\n```\n\n---\n\n## See Also\n\n- [Error Handling Pattern 1: Accumulation](./error-handling-pattern-accumulation.mdx) - Multiple errors\n- [Error Handling Pattern 3: Custom Strategies](./error-handling-pattern-custom-strategies.mdx) - Custom error types\n- [Scheduling Pattern 5: Advanced Retries](./scheduling-pattern-advanced-retry-chains.mdx) - Retry with recovery\n- [Stream Pattern 6: Resource Management](./stream-pattern-resource-management.mdx) - Safe cleanup on error" - }, - { - "id": "error-handling-pattern-custom-strategies", - "title": "Error Handling Pattern 3: Custom Error Strategies", - "description": "Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies.", - "skillLevel": "advanced", - "useCases": [ - "error-handling" - ], - "example": "This example demonstrates custom error strategies.\n\n```typescript\nimport { Effect, Data, Schedule } from \"effect\";\n\n// Custom domain errors\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n endpoint: string;\n statusCode?: number;\n retryable: boolean;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n field: string;\n reason: string;\n}> {}\n\nclass AuthenticationError extends Data.TaggedError(\"AuthenticationError\")<{\n reason: \"invalid-token\" | \"expired-token\" | \"missing-token\";\n}> {}\n\nclass PermissionError extends Data.TaggedError(\"PermissionError\")<{\n resource: string;\n action: string;\n}> {}\n\nclass RateLimitError extends Data.TaggedError(\"RateLimitError\")<{\n retryAfter: number; // milliseconds\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n resource: string;\n id: string;\n}> {}\n\n// Recovery strategy selector\nconst selectRecoveryStrategy = (\n error: Error\n): \"retry\" | \"fallback\" | \"fail\" | \"user-message\" => {\n if (error instanceof NetworkError && error.retryable) {\n return \"retry\";\n }\n\n if (error instanceof RateLimitError) {\n return \"retry\"; // With backoff\n }\n\n if (error instanceof ValidationError) {\n return \"user-message\"; // User can fix\n }\n\n if (error instanceof NotFoundError) {\n return \"fallback\"; // Use empty result\n }\n\n if (\n error instanceof AuthenticationError &&\n error.reason === \"expired-token\"\n ) {\n return \"retry\"; // Refresh token\n }\n\n if (error instanceof PermissionError) {\n return \"fail\"; // Don't retry\n }\n\n return \"fail\"; // Default: don't retry\n};\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[CUSTOM ERROR STRATEGIES] Domain-aware error handling\\n`\n );\n\n // Example 1: Type-safe error handling\n console.log(`[1] Type-safe error catching:\\n`);\n\n const operation1 = Effect.fail(\n new ValidationError({\n field: \"email\",\n reason: \"Invalid format\",\n })\n );\n\n const handled1 = operation1.pipe(\n Effect.catchTag(\"ValidationError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CAUGHT] Validation error`);\n yield* Effect.log(` Field: ${error.field}`);\n yield* Effect.log(` Reason: ${error.reason}\\n`);\n\n return \"validation-failed\";\n })\n )\n );\n\n yield* handled1;\n\n // Example 2: Multiple error types with different recovery\n console.log(`[2] Different recovery per error type:\\n`);\n\n interface ApiResponse {\n status: number;\n body?: unknown;\n }\n\n const callApi = (shouldFail: \"network\" | \"validation\" | \"ratelimit\" | \"success\") =>\n Effect.gen(function* () {\n switch (shouldFail) {\n case \"network\":\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"https://api.example.com/data\",\n statusCode: 503,\n retryable: true,\n })\n );\n\n case \"validation\":\n yield* Effect.fail(\n new ValidationError({\n field: \"id\",\n reason: \"Must be numeric\",\n })\n );\n\n case \"ratelimit\":\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 5000,\n })\n );\n\n case \"success\":\n return { status: 200, body: { id: 123 } };\n }\n });\n\n // Test each error type\n const testCases = [\"network\", \"validation\", \"ratelimit\", \"success\"] as const;\n\n for (const testCase of testCases) {\n const strategy = yield* callApi(testCase).pipe(\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[NETWORK] Retryable: ${error.retryable}, Status: ${error.statusCode}`\n );\n\n return \"will-retry\";\n })\n ),\n Effect.catchTag(\"ValidationError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[VALIDATION] ${error.field}: ${error.reason} (no retry)`\n );\n\n return \"user-must-fix\";\n })\n ),\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[RATE-LIMIT] Retry after ${error.retryAfter}ms`\n );\n\n return \"retry-with-backoff\";\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[SUCCESS] Got response`);\n\n return \"completed\";\n })\n )\n );\n\n yield* Effect.log(` Strategy: ${strategy}\\n`);\n }\n\n // Example 3: Custom retry strategy based on error\n console.log(`[3] Error-specific retry strategies:\\n`);\n\n let attemptCount = 0;\n\n const networkOperation = Effect.gen(function* () {\n attemptCount++;\n\n yield* Effect.log(`[ATTEMPT] ${attemptCount}`);\n\n if (attemptCount === 1) {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"api.example.com\",\n statusCode: 502,\n retryable: true,\n })\n );\n }\n\n if (attemptCount === 2) {\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 100,\n })\n );\n }\n\n return \"success\";\n });\n\n // Type-safe retry with error classification\n let result3: string | null = null;\n\n for (let i = 0; i < 3; i++) {\n result3 = yield* networkOperation.pipe(\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n if (error.retryable && i < 2) {\n yield* Effect.log(`[RETRY] Network error is retryable`);\n\n return null; // Signal to retry\n }\n\n yield* Effect.log(`[FAIL] Network error not retryable`);\n\n return Effect.fail(error);\n })\n ),\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BACKOFF] Rate limited, waiting ${error.retryAfter}ms`\n );\n\n yield* Effect.sleep(`${error.retryAfter} millis`);\n\n return null; // Signal to retry\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Unhandled: ${error}`);\n\n return Effect.fail(error);\n })\n )\n ).pipe(\n Effect.catchAll(() => Effect.succeed(null))\n );\n\n if (result3 !== null) {\n break;\n }\n }\n\n yield* Effect.log(`\\n[RESULT] ${result3}\\n`);\n\n // Example 4: Error-aware business logic\n console.log(`[4] Business logic with error handling:\\n`);\n\n interface User {\n id: string;\n email: string;\n }\n\n const loadUser = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n if (id === \"invalid\") {\n yield* Effect.fail(\n new NotFoundError({\n resource: \"user\",\n id,\n })\n );\n }\n\n if (id === \"network-error\") {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"/api/users\",\n retryable: true,\n })\n );\n }\n\n return { id, email: `user-${id}@example.com` };\n });\n\n const processUser = (id: string) =>\n loadUser(id).pipe(\n Effect.catchTag(\"NotFoundError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BUSINESS] User not found: ${error.id}`\n );\n\n // Return default/empty user\n return { id: \"\", email: \"\" };\n })\n ),\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BUSINESS] Network error, will retry from cache`\n );\n\n return { id, email: \"cached@example.com\" };\n })\n )\n );\n\n yield* processUser(\"valid-id\");\n\n yield* processUser(\"invalid\");\n\n yield* processUser(\"network-error\");\n\n // Example 5: Discriminated union for exhaustiveness\n console.log(`\\n[5] Exhaustiveness checking (compile-time safety):\\n`);\n\n const classifyError = (\n error: NetworkError | ValidationError | AuthenticationError | PermissionError\n ): string => {\n switch (error._tag) {\n case \"NetworkError\":\n return `network: ${error.statusCode}`;\n\n case \"ValidationError\":\n return `validation: ${error.field}`;\n\n case \"AuthenticationError\":\n return `auth: ${error.reason}`;\n\n case \"PermissionError\":\n return `permission: ${error.action}`;\n\n // TypeScript ensures all cases covered\n default:\n const _exhaustive: never = error;\n return _exhaustive;\n }\n };\n\n const testError = new ValidationError({\n field: \"age\",\n reason: \"Must be >= 18\",\n });\n\n const classification = classifyError(testError);\n\n yield* Effect.log(`[CLASSIFY] ${classification}`);\n\n // Example 6: Recovery strategy chains\n console.log(`\\n[6] Chained recovery strategies:\\n`);\n\n const resilientOperation = Effect.gen(function* () {\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 50,\n })\n );\n });\n\n const withRecovery = resilientOperation.pipe(\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[STEP 1] Caught rate limit, waiting ${error.retryAfter}ms`\n );\n\n yield* Effect.sleep(`${error.retryAfter} millis`);\n\n // Try again\n return yield* Effect.succeed(\"recovered\");\n })\n ),\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n if (error.retryable) {\n yield* Effect.log(`[STEP 2] Network error, retrying...`);\n\n return \"retry\";\n }\n\n return yield* Effect.fail(error);\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STEP 3] Final fallback`);\n\n return \"fallback\";\n })\n )\n );\n\n yield* withRecovery;\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Generic errors prevent optimal recovery:\n\n**Problem 1: One-size-fits-all retry**\n- Network timeout (transient, retry with backoff)\n- Invalid API key (permanent, don't retry)\n- Both treated same = wrong recovery\n\n**Problem 2: Lost business intent**\n- System error: \"Connection refused\"\n- Business meaning: Unclear\n- User message: \"Something went wrong\" (not helpful)\n\n**Problem 3: Wrong recovery layer**\n- Should retry at network layer\n- Instead retried at application layer\n- Wasted compute, poor user experience\n\n**Problem 4: Silent failures**\n- Multiple error types possible\n- Generic catch ignores distinctions\n- Bug: handled Error A as if it were Error B\n- Data corruption, hard to debug\n\nSolutions:\n\n**Tagged errors**:\n- `NetworkError`, `ValidationError`, `PermissionError`\n- Type system ensures handling\n- TypeScript compiler catches missed cases\n- Clear intent\n\n**Recovery strategies**:\n- `NetworkError` → Retry with exponential backoff\n- `ValidationError` → Return user message, no retry\n- `PermissionError` → Log security event, no retry\n- `TemporaryError` → Retry with jitter\n\n**Business semantics**:\n- Error type matches domain concept\n- Code reads like domain language\n- Easier to maintain\n- New developers understand quickly\n\n---", - "content": "## Guideline\n\nCustom error strategies enable business logic:\n\n- **Tagged errors**: Effect.Data for type-safe errors\n- **Error classification**: Retryable, transient, permanent\n- **Domain semantics**: Business-meaning errors\n- **Recovery strategies**: Different per error type\n- **Error context**: Includes recovery hints\n\nPattern: Use `Data.TaggedError`, error discriminators, `catchTag()`\n\n---\n\n## Rationale\n\nGeneric errors prevent optimal recovery:\n\n**Problem 1: One-size-fits-all retry**\n- Network timeout (transient, retry with backoff)\n- Invalid API key (permanent, don't retry)\n- Both treated same = wrong recovery\n\n**Problem 2: Lost business intent**\n- System error: \"Connection refused\"\n- Business meaning: Unclear\n- User message: \"Something went wrong\" (not helpful)\n\n**Problem 3: Wrong recovery layer**\n- Should retry at network layer\n- Instead retried at application layer\n- Wasted compute, poor user experience\n\n**Problem 4: Silent failures**\n- Multiple error types possible\n- Generic catch ignores distinctions\n- Bug: handled Error A as if it were Error B\n- Data corruption, hard to debug\n\nSolutions:\n\n**Tagged errors**:\n- `NetworkError`, `ValidationError`, `PermissionError`\n- Type system ensures handling\n- TypeScript compiler catches missed cases\n- Clear intent\n\n**Recovery strategies**:\n- `NetworkError` → Retry with exponential backoff\n- `ValidationError` → Return user message, no retry\n- `PermissionError` → Log security event, no retry\n- `TemporaryError` → Retry with jitter\n\n**Business semantics**:\n- Error type matches domain concept\n- Code reads like domain language\n- Easier to maintain\n- New developers understand quickly\n\n---\n\n## Good Example\n\nThis example demonstrates custom error strategies.\n\n```typescript\nimport { Effect, Data, Schedule } from \"effect\";\n\n// Custom domain errors\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n endpoint: string;\n statusCode?: number;\n retryable: boolean;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n field: string;\n reason: string;\n}> {}\n\nclass AuthenticationError extends Data.TaggedError(\"AuthenticationError\")<{\n reason: \"invalid-token\" | \"expired-token\" | \"missing-token\";\n}> {}\n\nclass PermissionError extends Data.TaggedError(\"PermissionError\")<{\n resource: string;\n action: string;\n}> {}\n\nclass RateLimitError extends Data.TaggedError(\"RateLimitError\")<{\n retryAfter: number; // milliseconds\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n resource: string;\n id: string;\n}> {}\n\n// Recovery strategy selector\nconst selectRecoveryStrategy = (\n error: Error\n): \"retry\" | \"fallback\" | \"fail\" | \"user-message\" => {\n if (error instanceof NetworkError && error.retryable) {\n return \"retry\";\n }\n\n if (error instanceof RateLimitError) {\n return \"retry\"; // With backoff\n }\n\n if (error instanceof ValidationError) {\n return \"user-message\"; // User can fix\n }\n\n if (error instanceof NotFoundError) {\n return \"fallback\"; // Use empty result\n }\n\n if (\n error instanceof AuthenticationError &&\n error.reason === \"expired-token\"\n ) {\n return \"retry\"; // Refresh token\n }\n\n if (error instanceof PermissionError) {\n return \"fail\"; // Don't retry\n }\n\n return \"fail\"; // Default: don't retry\n};\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[CUSTOM ERROR STRATEGIES] Domain-aware error handling\\n`\n );\n\n // Example 1: Type-safe error handling\n console.log(`[1] Type-safe error catching:\\n`);\n\n const operation1 = Effect.fail(\n new ValidationError({\n field: \"email\",\n reason: \"Invalid format\",\n })\n );\n\n const handled1 = operation1.pipe(\n Effect.catchTag(\"ValidationError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CAUGHT] Validation error`);\n yield* Effect.log(` Field: ${error.field}`);\n yield* Effect.log(` Reason: ${error.reason}\\n`);\n\n return \"validation-failed\";\n })\n )\n );\n\n yield* handled1;\n\n // Example 2: Multiple error types with different recovery\n console.log(`[2] Different recovery per error type:\\n`);\n\n interface ApiResponse {\n status: number;\n body?: unknown;\n }\n\n const callApi = (shouldFail: \"network\" | \"validation\" | \"ratelimit\" | \"success\") =>\n Effect.gen(function* () {\n switch (shouldFail) {\n case \"network\":\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"https://api.example.com/data\",\n statusCode: 503,\n retryable: true,\n })\n );\n\n case \"validation\":\n yield* Effect.fail(\n new ValidationError({\n field: \"id\",\n reason: \"Must be numeric\",\n })\n );\n\n case \"ratelimit\":\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 5000,\n })\n );\n\n case \"success\":\n return { status: 200, body: { id: 123 } };\n }\n });\n\n // Test each error type\n const testCases = [\"network\", \"validation\", \"ratelimit\", \"success\"] as const;\n\n for (const testCase of testCases) {\n const strategy = yield* callApi(testCase).pipe(\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[NETWORK] Retryable: ${error.retryable}, Status: ${error.statusCode}`\n );\n\n return \"will-retry\";\n })\n ),\n Effect.catchTag(\"ValidationError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[VALIDATION] ${error.field}: ${error.reason} (no retry)`\n );\n\n return \"user-must-fix\";\n })\n ),\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[RATE-LIMIT] Retry after ${error.retryAfter}ms`\n );\n\n return \"retry-with-backoff\";\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[SUCCESS] Got response`);\n\n return \"completed\";\n })\n )\n );\n\n yield* Effect.log(` Strategy: ${strategy}\\n`);\n }\n\n // Example 3: Custom retry strategy based on error\n console.log(`[3] Error-specific retry strategies:\\n`);\n\n let attemptCount = 0;\n\n const networkOperation = Effect.gen(function* () {\n attemptCount++;\n\n yield* Effect.log(`[ATTEMPT] ${attemptCount}`);\n\n if (attemptCount === 1) {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"api.example.com\",\n statusCode: 502,\n retryable: true,\n })\n );\n }\n\n if (attemptCount === 2) {\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 100,\n })\n );\n }\n\n return \"success\";\n });\n\n // Type-safe retry with error classification\n let result3: string | null = null;\n\n for (let i = 0; i < 3; i++) {\n result3 = yield* networkOperation.pipe(\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n if (error.retryable && i < 2) {\n yield* Effect.log(`[RETRY] Network error is retryable`);\n\n return null; // Signal to retry\n }\n\n yield* Effect.log(`[FAIL] Network error not retryable`);\n\n return Effect.fail(error);\n })\n ),\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BACKOFF] Rate limited, waiting ${error.retryAfter}ms`\n );\n\n yield* Effect.sleep(`${error.retryAfter} millis`);\n\n return null; // Signal to retry\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Unhandled: ${error}`);\n\n return Effect.fail(error);\n })\n )\n ).pipe(\n Effect.catchAll(() => Effect.succeed(null))\n );\n\n if (result3 !== null) {\n break;\n }\n }\n\n yield* Effect.log(`\\n[RESULT] ${result3}\\n`);\n\n // Example 4: Error-aware business logic\n console.log(`[4] Business logic with error handling:\\n`);\n\n interface User {\n id: string;\n email: string;\n }\n\n const loadUser = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n if (id === \"invalid\") {\n yield* Effect.fail(\n new NotFoundError({\n resource: \"user\",\n id,\n })\n );\n }\n\n if (id === \"network-error\") {\n yield* Effect.fail(\n new NetworkError({\n endpoint: \"/api/users\",\n retryable: true,\n })\n );\n }\n\n return { id, email: `user-${id}@example.com` };\n });\n\n const processUser = (id: string) =>\n loadUser(id).pipe(\n Effect.catchTag(\"NotFoundError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BUSINESS] User not found: ${error.id}`\n );\n\n // Return default/empty user\n return { id: \"\", email: \"\" };\n })\n ),\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[BUSINESS] Network error, will retry from cache`\n );\n\n return { id, email: \"cached@example.com\" };\n })\n )\n );\n\n yield* processUser(\"valid-id\");\n\n yield* processUser(\"invalid\");\n\n yield* processUser(\"network-error\");\n\n // Example 5: Discriminated union for exhaustiveness\n console.log(`\\n[5] Exhaustiveness checking (compile-time safety):\\n`);\n\n const classifyError = (\n error: NetworkError | ValidationError | AuthenticationError | PermissionError\n ): string => {\n switch (error._tag) {\n case \"NetworkError\":\n return `network: ${error.statusCode}`;\n\n case \"ValidationError\":\n return `validation: ${error.field}`;\n\n case \"AuthenticationError\":\n return `auth: ${error.reason}`;\n\n case \"PermissionError\":\n return `permission: ${error.action}`;\n\n // TypeScript ensures all cases covered\n default:\n const _exhaustive: never = error;\n return _exhaustive;\n }\n };\n\n const testError = new ValidationError({\n field: \"age\",\n reason: \"Must be >= 18\",\n });\n\n const classification = classifyError(testError);\n\n yield* Effect.log(`[CLASSIFY] ${classification}`);\n\n // Example 6: Recovery strategy chains\n console.log(`\\n[6] Chained recovery strategies:\\n`);\n\n const resilientOperation = Effect.gen(function* () {\n yield* Effect.fail(\n new RateLimitError({\n retryAfter: 50,\n })\n );\n });\n\n const withRecovery = resilientOperation.pipe(\n Effect.catchTag(\"RateLimitError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[STEP 1] Caught rate limit, waiting ${error.retryAfter}ms`\n );\n\n yield* Effect.sleep(`${error.retryAfter} millis`);\n\n // Try again\n return yield* Effect.succeed(\"recovered\");\n })\n ),\n Effect.catchTag(\"NetworkError\", (error) =>\n Effect.gen(function* () {\n if (error.retryable) {\n yield* Effect.log(`[STEP 2] Network error, retrying...`);\n\n return \"retry\";\n }\n\n return yield* Effect.fail(error);\n })\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STEP 3] Final fallback`);\n\n return \"fallback\";\n })\n )\n );\n\n yield* withRecovery;\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Error Classification Middleware\n\nBuild composable error handlers:\n\n```typescript\ninterface ErrorHandler {\n canHandle: (error: Error) => boolean;\n handle: (error: Error) => Effect.Effect;\n priority: number; // Higher = tried first\n}\n\nconst applyErrorHandlers = (\n effect: Effect.Effect,\n handlers: ErrorHandler[]\n) => {\n const sorted = handlers.sort((a, b) => b.priority - a.priority);\n\n return sorted.reduce(\n (current, handler) =>\n current.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (handler.canHandle(error as Error)) {\n return yield* handler.handle(error as Error);\n }\n\n return yield* Effect.fail(error);\n })\n )\n ),\n effect\n );\n};\n\n// Usage\nconst handlers: ErrorHandler[] = [\n {\n priority: 100,\n canHandle: (e) => e instanceof RateLimitError,\n handle: (e) =>\n Effect.log(\n `Handling rate limit: ${(e as RateLimitError).retryAfter}ms`\n ),\n },\n {\n priority: 50,\n canHandle: (e) => e instanceof NetworkError,\n handle: (e) =>\n Effect.log(`Handling network error`),\n },\n];\n```\n\n---\n\n## Advanced: Semantic Error Recovery\n\nDomain-specific recovery logic:\n\n```typescript\ninterface RecoveryAction {\n type: \"retry\" | \"fallback\" | \"refresh\" | \"alert\" | \"fail\";\n config: Record;\n}\n\nconst determineRecoveryAction = (error: Error): RecoveryAction => {\n if (error instanceof RateLimitError) {\n return {\n type: \"retry\",\n config: {\n delay: error.retryAfter,\n strategy: \"exponential\",\n },\n };\n }\n\n if (error instanceof AuthenticationError && error.reason === \"expired-token\") {\n return {\n type: \"refresh\",\n config: {\n tokenEndpoint: \"/auth/refresh\",\n scope: \"read write\",\n },\n };\n }\n\n if (error instanceof PermissionError) {\n return {\n type: \"alert\",\n config: {\n severity: \"high\",\n message: `Access denied: ${error.action}`,\n },\n };\n }\n\n return { type: \"fail\", config: {} };\n};\n\nconst executeRecoveryAction = (action: RecoveryAction) =>\n Effect.gen(function* () {\n switch (action.type) {\n case \"retry\":\n yield* Effect.log(\n `[RECOVERY] Retrying with delay: ${action.config.delay}ms`\n );\n break;\n\n case \"refresh\":\n yield* Effect.log(`[RECOVERY] Refreshing token`);\n break;\n\n case \"alert\":\n yield* Effect.log(`[RECOVERY] Alert: ${action.config.message}`);\n break;\n\n case \"fail\":\n yield* Effect.fail(new Error(\"Unrecoverable error\"));\n }\n });\n```\n\n---\n\n## Error Type Hierarchy\n\n```\nError (base)\n├── NetworkError (retryable transient)\n├── RateLimitError (retryable with backoff)\n├── ValidationError (non-retryable user error)\n├── AuthenticationError\n│ ├── InvalidToken (refresh)\n│ ├── ExpiredToken (refresh)\n│ └── MissingToken (fail)\n├── PermissionError (non-retryable security)\n└── NotFoundError (non-retryable with fallback)\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use custom errors when:**\n- Multiple failure modes exist\n- Different recovery per error\n- Need domain semantics\n- TypeScript available\n- Type safety matters\n\n✅ **Use recovery strategies when:**\n- Complex retry logic needed\n- Different backoff per error\n- Business logic affects recovery\n- Audit trail required\n\n⚠️ **Trade-offs:**\n- More error classes to maintain\n- Learning curve for teams\n- More code upfront\n- Harder to add generic handlers\n\n---\n\n## Custom Error Design Checklist\n\n- ✅ Classify errors as retryable/non-retryable\n- ✅ Include recovery hints in error\n- ✅ Use discriminated unions\n- ✅ Test all error paths\n- ✅ Document recovery strategy\n- ✅ Version error types\n- ✅ Consider logging/metrics\n- ✅ Plan error evolution\n\n---\n\n## See Also\n\n- [Error Handling Pattern 1: Accumulation](./error-handling-pattern-accumulation.mdx) - Multiple errors\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error chains\n- [Scheduling Pattern 5: Advanced Retries](./scheduling-pattern-advanced-retry-chains.mdx) - Error-aware retry\n- [Concurrency Pattern 2: Rate Limit](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Rate limit handling" - }, - { - "id": "execute-with-runpromise", - "title": "Execute Asynchronous Effects with Effect.runPromise", - "description": "Execute asynchronous effects with Effect.runPromise.", - "skillLevel": "beginner", - "useCases": [ - "project-setup--execution" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.succeed(\"Hello, World!\").pipe(Effect.delay(\"1 second\"));\n\nconst promise = Effect.runPromise(program);\n\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(result); // Logs \"Hello, World!\" after 1 second.\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \n`Effect.runPromise` executes your effect and returns a Promise, making it\neasy to integrate with existing JavaScript async workflows.", - "antiPattern": "Never call `runPromise` inside another `Effect` composition. Effects are\nmeant to be composed together _before_ being run once at the end.", - "explanation": "`Effect.runPromise` is the bridge from the Effect world to the Promise-based\nworld of Node.js and browsers. If the Effect succeeds, the Promise resolves;\nif it fails, the Promise rejects.", - "content": "# Execute Asynchronous Effects with Effect.runPromise\n\n## Guideline\n\nTo execute an `Effect` that may be asynchronous and retrieve its result, use\n`Effect.runPromise`. This should only be done at the outermost layer of your\napplication.\n\n## Rationale\n\n`Effect.runPromise` is the bridge from the Effect world to the Promise-based\nworld of Node.js and browsers. If the Effect succeeds, the Promise resolves;\nif it fails, the Promise rejects.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.succeed(\"Hello, World!\").pipe(Effect.delay(\"1 second\"));\n\nconst promise = Effect.runPromise(program);\n\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(result); // Logs \"Hello, World!\" after 1 second.\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \n`Effect.runPromise` executes your effect and returns a Promise, making it\neasy to integrate with existing JavaScript async workflows.\n\n## Anti-Pattern\n\nNever call `runPromise` inside another `Effect` composition. Effects are\nmeant to be composed together _before_ being run once at the end." - }, - { - "id": "execute-long-running-apps-with-runfork", - "title": "Execute Long-Running Apps with Effect.runFork", - "description": "Use Effect.runFork to launch a long-running application as a manageable, detached fiber.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This example starts a simple \"server\" that runs forever. We use `runFork` to launch it and then use the returned `Fiber` to shut it down gracefully after 5 seconds.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\n// A server that listens for requests forever\nconst server = Effect.log(\"Server received a request.\").pipe(\n Effect.delay(\"1 second\"),\n Effect.forever\n);\n\nEffect.runSync(Effect.log(\"Starting server...\"));\n\n// Launch the server as a detached, top-level fiber\nconst appFiber = Effect.runFork(server);\n\n// In a real app, you would listen for OS signals.\n// Here, we simulate a shutdown signal after 5 seconds.\nsetTimeout(() => {\n const shutdownProgram = Effect.gen(function* () {\n yield* Effect.log(\"Shutdown signal received. Interrupting server fiber...\");\n // This ensures all cleanup logic within the server effect would run.\n yield* Fiber.interrupt(appFiber);\n });\n Effect.runPromise(shutdownProgram);\n}, 5000);\n```\n\n---", - "antiPattern": "Using `runFork` when you immediately need the result of the effect. If you call `runFork` and then immediately call `Fiber.join` on the result, you have simply implemented a more complex and less direct version of `runPromise`.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst someEffect = Effect.succeed(42);\n\n// ❌ WRONG: This is just a complicated way to write `Effect.runPromise(someEffect)`\nconst resultPromise = Effect.runFork(someEffect).pipe(\n Fiber.join,\n Effect.runPromise\n);\n```", - "explanation": "Unlike `Effect.runPromise`, which waits for the effect to complete, `Effect.runFork` starts the effect and immediately returns a `Fiber`. This is the ideal way to run an application that is meant to run forever, because it gives you a handle to the process.\n\nThe most critical use case for this is enabling graceful shutdown. You can start your application with `runFork`, and then set up listeners for OS signals (like `SIGINT` for Ctrl+C). When a shutdown signal is received, you call `Fiber.interrupt` on the application fiber, which guarantees that all finalizers (like closing database connections) are run before the process exits.\n\n---", - "content": "## Guideline\n\nTo launch a long-running application (like a server or daemon) as a non-blocking, top-level process, use `Effect.runFork`. It immediately returns a `Fiber` representing your running application, which you can use to manage its lifecycle.\n\n---\n\n## Rationale\n\nUnlike `Effect.runPromise`, which waits for the effect to complete, `Effect.runFork` starts the effect and immediately returns a `Fiber`. This is the ideal way to run an application that is meant to run forever, because it gives you a handle to the process.\n\nThe most critical use case for this is enabling graceful shutdown. You can start your application with `runFork`, and then set up listeners for OS signals (like `SIGINT` for Ctrl+C). When a shutdown signal is received, you call `Fiber.interrupt` on the application fiber, which guarantees that all finalizers (like closing database connections) are run before the process exits.\n\n---\n\n## Good Example\n\nThis example starts a simple \"server\" that runs forever. We use `runFork` to launch it and then use the returned `Fiber` to shut it down gracefully after 5 seconds.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\n// A server that listens for requests forever\nconst server = Effect.log(\"Server received a request.\").pipe(\n Effect.delay(\"1 second\"),\n Effect.forever\n);\n\nEffect.runSync(Effect.log(\"Starting server...\"));\n\n// Launch the server as a detached, top-level fiber\nconst appFiber = Effect.runFork(server);\n\n// In a real app, you would listen for OS signals.\n// Here, we simulate a shutdown signal after 5 seconds.\nsetTimeout(() => {\n const shutdownProgram = Effect.gen(function* () {\n yield* Effect.log(\"Shutdown signal received. Interrupting server fiber...\");\n // This ensures all cleanup logic within the server effect would run.\n yield* Fiber.interrupt(appFiber);\n });\n Effect.runPromise(shutdownProgram);\n}, 5000);\n```\n\n---\n\n## Anti-Pattern\n\nUsing `runFork` when you immediately need the result of the effect. If you call `runFork` and then immediately call `Fiber.join` on the result, you have simply implemented a more complex and less direct version of `runPromise`.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst someEffect = Effect.succeed(42);\n\n// ❌ WRONG: This is just a complicated way to write `Effect.runPromise(someEffect)`\nconst resultPromise = Effect.runFork(someEffect).pipe(\n Fiber.join,\n Effect.runPromise\n);\n```" - }, - { - "id": "execute-with-runsync", - "title": "Execute Synchronous Effects with Effect.runSync", - "description": "Execute synchronous effects with Effect.runSync.", - "skillLevel": "beginner", - "useCases": [ - "project-setup--execution" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Simple synchronous program\nconst program1 = Effect.gen(function* () {\n const n = 10;\n const result = n * 2;\n yield* Effect.log(`Simple program result: ${result}`);\n return result;\n});\n\n// Run simple program\nEffect.runSync(program1);\n\n// Program with logging\nconst program2 = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting calculation...\");\n const n = yield* Effect.sync(() => 10);\n yield* Effect.logInfo(`Got number: ${n}`);\n const result = yield* Effect.sync(() => n * 2);\n yield* Effect.logInfo(`Result: ${result}`);\n return result;\n});\n\n// Run with logging\nEffect.runSync(program2);\n\n// Program with error handling\nconst program3 = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting division...\");\n const n = yield* Effect.sync(() => 10);\n const divisor = yield* Effect.sync(() => 0);\n\n yield* Effect.logInfo(`Attempting to divide ${n} by ${divisor}...`);\n return yield* Effect.try({\n try: () => {\n if (divisor === 0) throw new Error(\"Cannot divide by zero\");\n return n / divisor;\n },\n catch: (error) => {\n if (error instanceof Error) {\n return error;\n }\n return new Error(\"Unknown error occurred\");\n },\n });\n}).pipe(\n Effect.catchAll((error) => Effect.logInfo(`Error occurred: ${error.message}`))\n);\n\n// Run with error handling\nEffect.runSync(program3);\n```\n\n**Explanation:** \nUse `runSync` only for Effects that are fully synchronous. If the Effect\ncontains async code, use `runPromise` instead.", - "antiPattern": "Do not use `runSync` on an Effect that contains asynchronous operations like\n`Effect.delay` or `Effect.promise`. This will result in a runtime error.", - "explanation": "`Effect.runSync` is an optimized runner for Effects that don't involve any\nasynchronous operations. If the Effect contains any async operations,\n`runSync` will throw an error.", - "content": "# Execute Synchronous Effects with Effect.runSync\n\n## Guideline\n\nTo execute an `Effect` that is guaranteed to be synchronous, use\n`Effect.runSync`. This will return the success value directly or throw the\nerror.\n\n## Rationale\n\n`Effect.runSync` is an optimized runner for Effects that don't involve any\nasynchronous operations. If the Effect contains any async operations,\n`runSync` will throw an error.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Simple synchronous program\nconst program1 = Effect.gen(function* () {\n const n = 10;\n const result = n * 2;\n yield* Effect.log(`Simple program result: ${result}`);\n return result;\n});\n\n// Run simple program\nEffect.runSync(program1);\n\n// Program with logging\nconst program2 = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting calculation...\");\n const n = yield* Effect.sync(() => 10);\n yield* Effect.logInfo(`Got number: ${n}`);\n const result = yield* Effect.sync(() => n * 2);\n yield* Effect.logInfo(`Result: ${result}`);\n return result;\n});\n\n// Run with logging\nEffect.runSync(program2);\n\n// Program with error handling\nconst program3 = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting division...\");\n const n = yield* Effect.sync(() => 10);\n const divisor = yield* Effect.sync(() => 0);\n\n yield* Effect.logInfo(`Attempting to divide ${n} by ${divisor}...`);\n return yield* Effect.try({\n try: () => {\n if (divisor === 0) throw new Error(\"Cannot divide by zero\");\n return n / divisor;\n },\n catch: (error) => {\n if (error instanceof Error) {\n return error;\n }\n return new Error(\"Unknown error occurred\");\n },\n });\n}).pipe(\n Effect.catchAll((error) => Effect.logInfo(`Error occurred: ${error.message}`))\n);\n\n// Run with error handling\nEffect.runSync(program3);\n```\n\n**Explanation:** \nUse `runSync` only for Effects that are fully synchronous. If the Effect\ncontains async code, use `runPromise` instead.\n\n## Anti-Pattern\n\nDo not use `runSync` on an Effect that contains asynchronous operations like\n`Effect.delay` or `Effect.promise`. This will result in a runtime error." - }, - { - "id": "observability-prometheus", - "title": "Export Metrics to Prometheus", - "description": "Use Effect metrics and expose a /metrics endpoint for Prometheus scraping.", - "skillLevel": "advanced", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, Metric, MetricLabel, Duration } from \"effect\"\nimport { HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define application metrics\n// ============================================\n\n// Counter - counts events\nconst httpRequestsTotal = Metric.counter(\"http_requests_total\", {\n description: \"Total number of HTTP requests\",\n})\n\n// Counter with labels\nconst httpRequestsByStatus = Metric.counter(\"http_requests_by_status\", {\n description: \"HTTP requests by status code\",\n})\n\n// Gauge - current value\nconst activeConnections = Metric.gauge(\"active_connections\", {\n description: \"Number of active connections\",\n})\n\n// Histogram - distribution of values\nconst requestDuration = Metric.histogram(\"http_request_duration_seconds\", {\n description: \"HTTP request duration in seconds\",\n boundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],\n})\n\n// Summary - percentiles\nconst responseSizeBytes = Metric.summary(\"http_response_size_bytes\", {\n description: \"HTTP response size in bytes\",\n maxAge: Duration.minutes(5),\n maxSize: 100,\n quantiles: [0.5, 0.9, 0.99],\n})\n\n// ============================================\n// 2. Instrument code with metrics\n// ============================================\n\nconst handleRequest = (path: string, status: number) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n // Increment request counter\n yield* Metric.increment(httpRequestsTotal)\n\n // Increment with labels\n yield* Metric.increment(\n httpRequestsByStatus.pipe(\n Metric.tagged(\"status\", String(status)),\n Metric.tagged(\"path\", path)\n )\n )\n\n // Track active connections\n yield* Metric.increment(activeConnections)\n\n // Simulate work\n yield* Effect.sleep(\"100 millis\")\n\n // Record duration\n const duration = (Date.now() - startTime) / 1000\n yield* Metric.update(requestDuration, duration)\n\n // Record response size\n yield* Metric.update(responseSizeBytes, 1024)\n\n // Decrement active connections\n yield* Metric.decrement(activeConnections)\n })\n\n// ============================================\n// 3. Prometheus text format exporter\n// ============================================\n\ninterface MetricSnapshot {\n name: string\n type: \"counter\" | \"gauge\" | \"histogram\" | \"summary\"\n help: string\n values: Array<{\n labels: Record\n value: number\n }>\n // For histograms\n buckets?: Array<{\n le: number\n count: number\n labels?: Record\n }>\n sum?: number\n count?: number\n}\n\nconst formatPrometheusMetrics = (metrics: MetricSnapshot[]): string => {\n const lines: string[] = []\n\n for (const metric of metrics) {\n // Help line\n lines.push(`# HELP ${metric.name} ${metric.help}`)\n lines.push(`# TYPE ${metric.name} ${metric.type}`)\n\n // Values\n for (const { labels, value } of metric.values) {\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=\"${v}\"`)\n .join(\",\")\n\n if (labelStr) {\n lines.push(`${metric.name}{${labelStr}} ${value}`)\n } else {\n lines.push(`${metric.name} ${value}`)\n }\n }\n\n // Histogram buckets\n if (metric.buckets) {\n for (const bucket of metric.buckets) {\n const labelStr = Object.entries(bucket.labels || {})\n .map(([k, v]) => `${k}=\"${v}\"`)\n .concat([`le=\"${bucket.le}\"`])\n .join(\",\")\n lines.push(`${metric.name}_bucket{${labelStr}} ${bucket.count}`)\n }\n lines.push(`${metric.name}_sum ${metric.sum}`)\n lines.push(`${metric.name}_count ${metric.count}`)\n }\n\n lines.push(\"\")\n }\n\n return lines.join(\"\\n\")\n}\n\n// ============================================\n// 4. /metrics endpoint handler\n// ============================================\n\nconst metricsHandler = Effect.gen(function* () {\n // In real implementation, read from Effect's MetricRegistry\n const metrics: MetricSnapshot[] = [\n {\n name: \"http_requests_total\",\n type: \"counter\",\n help: \"Total number of HTTP requests\",\n values: [{ labels: {}, value: 1234 }],\n },\n {\n name: \"http_requests_by_status\",\n type: \"counter\",\n help: \"HTTP requests by status code\",\n values: [\n { labels: { status: \"200\", path: \"/api/users\" }, value: 1000 },\n { labels: { status: \"404\", path: \"/api/users\" }, value: 50 },\n { labels: { status: \"500\", path: \"/api/users\" }, value: 10 },\n ],\n },\n {\n name: \"active_connections\",\n type: \"gauge\",\n help: \"Number of active connections\",\n values: [{ labels: {}, value: 42 }],\n },\n {\n name: \"http_request_duration_seconds\",\n type: \"histogram\",\n help: \"HTTP request duration in seconds\",\n values: [],\n buckets: [\n { le: 0.01, count: 100 },\n { le: 0.05, count: 500 },\n { le: 0.1, count: 800 },\n { le: 0.25, count: 950 },\n { le: 0.5, count: 990 },\n { le: 1, count: 999 },\n { le: Infinity, count: 1000 },\n ],\n sum: 123.456,\n count: 1000,\n },\n ]\n\n const body = formatPrometheusMetrics(metrics)\n\n return HttpServerResponse.text(body, {\n headers: {\n \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\",\n },\n })\n})\n\n// ============================================\n// 5. Example output\n// ============================================\n\n/*\n# HELP http_requests_total Total number of HTTP requests\n# TYPE http_requests_total counter\nhttp_requests_total 1234\n\n# HELP http_requests_by_status HTTP requests by status code\n# TYPE http_requests_by_status counter\nhttp_requests_by_status{status=\"200\",path=\"/api/users\"} 1000\nhttp_requests_by_status{status=\"404\",path=\"/api/users\"} 50\nhttp_requests_by_status{status=\"500\",path=\"/api/users\"} 10\n\n# HELP active_connections Number of active connections\n# TYPE active_connections gauge\nactive_connections 42\n\n# HELP http_request_duration_seconds HTTP request duration in seconds\n# TYPE http_request_duration_seconds histogram\nhttp_request_duration_seconds_bucket{le=\"0.01\"} 100\nhttp_request_duration_seconds_bucket{le=\"0.05\"} 500\nhttp_request_duration_seconds_bucket{le=\"0.1\"} 800\nhttp_request_duration_seconds_bucket{le=\"+Inf\"} 1000\nhttp_request_duration_seconds_sum 123.456\nhttp_request_duration_seconds_count 1000\n*/\n```", - "antiPattern": "", - "explanation": "Prometheus metrics enable:\n\n1. **Real-time monitoring** - See what's happening now\n2. **Historical analysis** - Track trends over time\n3. **Alerting** - Get notified of issues\n4. **Dashboards** - Visualize system health\n\n---", - "content": "## Guideline\n\nCreate metrics with Effect's Metric API and expose them via an HTTP endpoint in Prometheus text format.\n\n---\n\n## Rationale\n\nPrometheus metrics enable:\n\n1. **Real-time monitoring** - See what's happening now\n2. **Historical analysis** - Track trends over time\n3. **Alerting** - Get notified of issues\n4. **Dashboards** - Visualize system health\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Metric, MetricLabel, Duration } from \"effect\"\nimport { HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define application metrics\n// ============================================\n\n// Counter - counts events\nconst httpRequestsTotal = Metric.counter(\"http_requests_total\", {\n description: \"Total number of HTTP requests\",\n})\n\n// Counter with labels\nconst httpRequestsByStatus = Metric.counter(\"http_requests_by_status\", {\n description: \"HTTP requests by status code\",\n})\n\n// Gauge - current value\nconst activeConnections = Metric.gauge(\"active_connections\", {\n description: \"Number of active connections\",\n})\n\n// Histogram - distribution of values\nconst requestDuration = Metric.histogram(\"http_request_duration_seconds\", {\n description: \"HTTP request duration in seconds\",\n boundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],\n})\n\n// Summary - percentiles\nconst responseSizeBytes = Metric.summary(\"http_response_size_bytes\", {\n description: \"HTTP response size in bytes\",\n maxAge: Duration.minutes(5),\n maxSize: 100,\n quantiles: [0.5, 0.9, 0.99],\n})\n\n// ============================================\n// 2. Instrument code with metrics\n// ============================================\n\nconst handleRequest = (path: string, status: number) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n // Increment request counter\n yield* Metric.increment(httpRequestsTotal)\n\n // Increment with labels\n yield* Metric.increment(\n httpRequestsByStatus.pipe(\n Metric.tagged(\"status\", String(status)),\n Metric.tagged(\"path\", path)\n )\n )\n\n // Track active connections\n yield* Metric.increment(activeConnections)\n\n // Simulate work\n yield* Effect.sleep(\"100 millis\")\n\n // Record duration\n const duration = (Date.now() - startTime) / 1000\n yield* Metric.update(requestDuration, duration)\n\n // Record response size\n yield* Metric.update(responseSizeBytes, 1024)\n\n // Decrement active connections\n yield* Metric.decrement(activeConnections)\n })\n\n// ============================================\n// 3. Prometheus text format exporter\n// ============================================\n\ninterface MetricSnapshot {\n name: string\n type: \"counter\" | \"gauge\" | \"histogram\" | \"summary\"\n help: string\n values: Array<{\n labels: Record\n value: number\n }>\n // For histograms\n buckets?: Array<{\n le: number\n count: number\n labels?: Record\n }>\n sum?: number\n count?: number\n}\n\nconst formatPrometheusMetrics = (metrics: MetricSnapshot[]): string => {\n const lines: string[] = []\n\n for (const metric of metrics) {\n // Help line\n lines.push(`# HELP ${metric.name} ${metric.help}`)\n lines.push(`# TYPE ${metric.name} ${metric.type}`)\n\n // Values\n for (const { labels, value } of metric.values) {\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=\"${v}\"`)\n .join(\",\")\n\n if (labelStr) {\n lines.push(`${metric.name}{${labelStr}} ${value}`)\n } else {\n lines.push(`${metric.name} ${value}`)\n }\n }\n\n // Histogram buckets\n if (metric.buckets) {\n for (const bucket of metric.buckets) {\n const labelStr = Object.entries(bucket.labels || {})\n .map(([k, v]) => `${k}=\"${v}\"`)\n .concat([`le=\"${bucket.le}\"`])\n .join(\",\")\n lines.push(`${metric.name}_bucket{${labelStr}} ${bucket.count}`)\n }\n lines.push(`${metric.name}_sum ${metric.sum}`)\n lines.push(`${metric.name}_count ${metric.count}`)\n }\n\n lines.push(\"\")\n }\n\n return lines.join(\"\\n\")\n}\n\n// ============================================\n// 4. /metrics endpoint handler\n// ============================================\n\nconst metricsHandler = Effect.gen(function* () {\n // In real implementation, read from Effect's MetricRegistry\n const metrics: MetricSnapshot[] = [\n {\n name: \"http_requests_total\",\n type: \"counter\",\n help: \"Total number of HTTP requests\",\n values: [{ labels: {}, value: 1234 }],\n },\n {\n name: \"http_requests_by_status\",\n type: \"counter\",\n help: \"HTTP requests by status code\",\n values: [\n { labels: { status: \"200\", path: \"/api/users\" }, value: 1000 },\n { labels: { status: \"404\", path: \"/api/users\" }, value: 50 },\n { labels: { status: \"500\", path: \"/api/users\" }, value: 10 },\n ],\n },\n {\n name: \"active_connections\",\n type: \"gauge\",\n help: \"Number of active connections\",\n values: [{ labels: {}, value: 42 }],\n },\n {\n name: \"http_request_duration_seconds\",\n type: \"histogram\",\n help: \"HTTP request duration in seconds\",\n values: [],\n buckets: [\n { le: 0.01, count: 100 },\n { le: 0.05, count: 500 },\n { le: 0.1, count: 800 },\n { le: 0.25, count: 950 },\n { le: 0.5, count: 990 },\n { le: 1, count: 999 },\n { le: Infinity, count: 1000 },\n ],\n sum: 123.456,\n count: 1000,\n },\n ]\n\n const body = formatPrometheusMetrics(metrics)\n\n return HttpServerResponse.text(body, {\n headers: {\n \"Content-Type\": \"text/plain; version=0.0.4; charset=utf-8\",\n },\n })\n})\n\n// ============================================\n// 5. Example output\n// ============================================\n\n/*\n# HELP http_requests_total Total number of HTTP requests\n# TYPE http_requests_total counter\nhttp_requests_total 1234\n\n# HELP http_requests_by_status HTTP requests by status code\n# TYPE http_requests_by_status counter\nhttp_requests_by_status{status=\"200\",path=\"/api/users\"} 1000\nhttp_requests_by_status{status=\"404\",path=\"/api/users\"} 50\nhttp_requests_by_status{status=\"500\",path=\"/api/users\"} 10\n\n# HELP active_connections Number of active connections\n# TYPE active_connections gauge\nactive_connections 42\n\n# HELP http_request_duration_seconds HTTP request duration in seconds\n# TYPE http_request_duration_seconds histogram\nhttp_request_duration_seconds_bucket{le=\"0.01\"} 100\nhttp_request_duration_seconds_bucket{le=\"0.05\"} 500\nhttp_request_duration_seconds_bucket{le=\"0.1\"} 800\nhttp_request_duration_seconds_bucket{le=\"+Inf\"} 1000\nhttp_request_duration_seconds_sum 123.456\nhttp_request_duration_seconds_count 1000\n*/\n```\n\n## Metric Types\n\n| Type | Use For |\n|------|---------|\n| **Counter** | Events that only increase |\n| **Gauge** | Values that go up and down |\n| **Histogram** | Distribution of values |\n| **Summary** | Percentiles of values |\n\n## Prometheus Config\n\n```yaml\n# prometheus.yml\nscrape_configs:\n - job_name: 'effect-app'\n scrape_interval: 15s\n static_configs:\n - targets: ['localhost:3000']\n metrics_path: '/metrics'\n```\n\n## Best Practices\n\n1. **Name metrics well** - `{namespace}_{subsystem}_{name}_{unit}`\n2. **Use labels sparingly** - High cardinality = problems\n3. **Include help text** - Document what metrics mean\n4. **Choose right type** - Counter vs gauge matters\n5. **Set appropriate buckets** - Match your SLOs" - }, - { - "id": "extract-path-parameters", - "title": "Extract Path Parameters", - "description": "Define routes with colon-prefixed parameters (e.g., /users/:id) and access their values within the handler.", - "skillLevel": "beginner", - "useCases": [ - "building-apis" - ], - "example": "This example defines a route that captures a `userId`. The handler for this route accesses the parsed parameters and uses the `userId` to construct a personalized greeting. The router automatically makes the parameters available to the handler.\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define tagged error for invalid paths\ninterface InvalidPathErrorSchema {\n readonly _tag: \"InvalidPathError\";\n readonly path: string;\n}\n\nconst makeInvalidPathError = (path: string): InvalidPathErrorSchema => ({\n _tag: \"InvalidPathError\",\n path,\n});\n\n// Define service interface\ninterface PathOps {\n readonly extractUserId: (\n path: string\n ) => Effect.Effect;\n readonly greetUser: (userId: string) => Effect.Effect;\n}\n\n// Create service\nclass PathService extends Effect.Service()(\"PathService\", {\n sync: () => ({\n extractUserId: (path: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\n `Attempting to extract user ID from path: ${path}`\n );\n\n const match = path.match(/\\/users\\/([^/]+)/);\n if (!match) {\n yield* Effect.logInfo(`No user ID found in path: ${path}`);\n return yield* Effect.fail(makeInvalidPathError(path));\n }\n\n const userId = match[1];\n yield* Effect.logInfo(`Successfully extracted user ID: ${userId}`);\n return userId;\n }),\n\n greetUser: (userId: string) =>\n Effect.gen(function* () {\n const greeting = `Hello, user ${userId}!`;\n yield* Effect.logInfo(greeting);\n return greeting;\n }),\n }),\n}) {}\n\n// Compose the functions with proper error handling\nconst processPath = (\n path: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const pathService = yield* PathService;\n yield* Effect.logInfo(`Processing path: ${path}`);\n const userId = yield* pathService.extractUserId(path);\n return yield* pathService.greetUser(userId);\n });\n\n// Run examples with proper error handling\nconst program = Effect.gen(function* () {\n // Test valid paths\n yield* Effect.logInfo(\"=== Testing valid paths ===\");\n const result1 = yield* processPath(\"/users/123\");\n yield* Effect.logInfo(`Result 1: ${result1}`);\n\n const result2 = yield* processPath(\"/users/abc\");\n yield* Effect.logInfo(`Result 2: ${result2}`);\n\n // Test invalid path\n yield* Effect.logInfo(\"\\n=== Testing invalid path ===\");\n const result3 = yield* processPath(\"/invalid/path\").pipe(\n Effect.catchTag(\"InvalidPathError\", (error) =>\n Effect.succeed(`Error: Invalid path ${error.path}`)\n )\n );\n yield* Effect.logInfo(result3);\n});\n\nEffect.runPromise(Effect.provide(program, PathService.Default));\n```", - "antiPattern": "The anti-pattern is to manually parse the URL string inside the handler. This approach is brittle, imperative, and mixes concerns.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// This route matches any sub-path of /users/, forcing manual parsing.\nconst app = Http.router.get(\n \"/users/*\", // Using a wildcard\n Http.request.ServerRequest.pipe(\n Effect.flatMap((req) => {\n // Manually split the URL to find the ID.\n const parts = req.url.split(\"/\"); // e.g., ['', 'users', '123']\n if (parts.length === 3 && parts[2]) {\n const userId = parts[2];\n return Http.response.text(`Hello, user ${userId}!`);\n }\n // Manual handling for missing ID.\n return Http.response.empty({ status: 404 });\n })\n )\n);\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual method is highly discouraged. It's fragile—a change in the base path or an extra slash could break the logic (`parts[2]`). It's also not declarative; the intent is hidden inside imperative code. The router's built-in parameter handling is safer, clearer, and the correct approach.", - "explanation": "APIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `Http.router` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing.\n\nBy defining parameters directly in the path string, you gain several benefits:\n\n1. **Declarative**: The route's structure is immediately obvious from its definition. The code clearly states, \"this route expects a dynamic segment here.\"\n2. **Safe and Robust**: The router handles the logic of extracting the parameter. This is less error-prone and more robust than manually splitting or using regular expressions on the URL string.\n3. **Clean Handler Logic**: The business logic inside your handler is separated from the concern of URL parsing. The handler simply receives the parameters it needs to do its job.\n4. **Composability**: This pattern composes perfectly with the rest of the `Http` module, allowing you to build complex and well-structured APIs.\n\n---", - "content": "## Guideline\n\nTo capture dynamic parts of a URL, define your route path with a colon-prefixed placeholder (e.g., `/users/:userId`) and access the parsed parameters within your handler `Effect`.\n\n---\n\n## Rationale\n\nAPIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `Http.router` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing.\n\nBy defining parameters directly in the path string, you gain several benefits:\n\n1. **Declarative**: The route's structure is immediately obvious from its definition. The code clearly states, \"this route expects a dynamic segment here.\"\n2. **Safe and Robust**: The router handles the logic of extracting the parameter. This is less error-prone and more robust than manually splitting or using regular expressions on the URL string.\n3. **Clean Handler Logic**: The business logic inside your handler is separated from the concern of URL parsing. The handler simply receives the parameters it needs to do its job.\n4. **Composability**: This pattern composes perfectly with the rest of the `Http` module, allowing you to build complex and well-structured APIs.\n\n---\n\n## Good Example\n\nThis example defines a route that captures a `userId`. The handler for this route accesses the parsed parameters and uses the `userId` to construct a personalized greeting. The router automatically makes the parameters available to the handler.\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define tagged error for invalid paths\ninterface InvalidPathErrorSchema {\n readonly _tag: \"InvalidPathError\";\n readonly path: string;\n}\n\nconst makeInvalidPathError = (path: string): InvalidPathErrorSchema => ({\n _tag: \"InvalidPathError\",\n path,\n});\n\n// Define service interface\ninterface PathOps {\n readonly extractUserId: (\n path: string\n ) => Effect.Effect;\n readonly greetUser: (userId: string) => Effect.Effect;\n}\n\n// Create service\nclass PathService extends Effect.Service()(\"PathService\", {\n sync: () => ({\n extractUserId: (path: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\n `Attempting to extract user ID from path: ${path}`\n );\n\n const match = path.match(/\\/users\\/([^/]+)/);\n if (!match) {\n yield* Effect.logInfo(`No user ID found in path: ${path}`);\n return yield* Effect.fail(makeInvalidPathError(path));\n }\n\n const userId = match[1];\n yield* Effect.logInfo(`Successfully extracted user ID: ${userId}`);\n return userId;\n }),\n\n greetUser: (userId: string) =>\n Effect.gen(function* () {\n const greeting = `Hello, user ${userId}!`;\n yield* Effect.logInfo(greeting);\n return greeting;\n }),\n }),\n}) {}\n\n// Compose the functions with proper error handling\nconst processPath = (\n path: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const pathService = yield* PathService;\n yield* Effect.logInfo(`Processing path: ${path}`);\n const userId = yield* pathService.extractUserId(path);\n return yield* pathService.greetUser(userId);\n });\n\n// Run examples with proper error handling\nconst program = Effect.gen(function* () {\n // Test valid paths\n yield* Effect.logInfo(\"=== Testing valid paths ===\");\n const result1 = yield* processPath(\"/users/123\");\n yield* Effect.logInfo(`Result 1: ${result1}`);\n\n const result2 = yield* processPath(\"/users/abc\");\n yield* Effect.logInfo(`Result 2: ${result2}`);\n\n // Test invalid path\n yield* Effect.logInfo(\"\\n=== Testing invalid path ===\");\n const result3 = yield* processPath(\"/invalid/path\").pipe(\n Effect.catchTag(\"InvalidPathError\", (error) =>\n Effect.succeed(`Error: Invalid path ${error.path}`)\n )\n );\n yield* Effect.logInfo(result3);\n});\n\nEffect.runPromise(Effect.provide(program, PathService.Default));\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to manually parse the URL string inside the handler. This approach is brittle, imperative, and mixes concerns.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// This route matches any sub-path of /users/, forcing manual parsing.\nconst app = Http.router.get(\n \"/users/*\", // Using a wildcard\n Http.request.ServerRequest.pipe(\n Effect.flatMap((req) => {\n // Manually split the URL to find the ID.\n const parts = req.url.split(\"/\"); // e.g., ['', 'users', '123']\n if (parts.length === 3 && parts[2]) {\n const userId = parts[2];\n return Http.response.text(`Hello, user ${userId}!`);\n }\n // Manual handling for missing ID.\n return Http.response.empty({ status: 404 });\n })\n )\n);\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual method is highly discouraged. It's fragile—a change in the base path or an extra slash could break the logic (`parts[2]`). It's also not declarative; the intent is hidden inside imperative code. The router's built-in parameter handling is safer, clearer, and the correct approach." - }, - { - "id": "pipeline-fan-out", - "title": "Fan Out to Multiple Consumers", - "description": "Use broadcast or partition to send stream data to multiple consumers.", - "skillLevel": "advanced", - "useCases": [ - "building-data-pipelines" - ], - "example": "```typescript\nimport { Effect, Stream, Queue, Fiber, Chunk } from \"effect\"\n\n// ============================================\n// 1. Broadcast to all consumers\n// ============================================\n\nconst broadcastExample = Effect.scoped(\n Effect.gen(function* () {\n const source = Stream.fromIterable([1, 2, 3, 4, 5])\n\n // Broadcast to 3 consumers - each gets all items\n const [stream1, stream2, stream3] = yield* Stream.broadcast(source, 3)\n\n // Consumer 1: Log items\n const consumer1 = stream1.pipe(\n Stream.tap((n) => Effect.log(`Consumer 1: ${n}`)),\n Stream.runDrain\n )\n\n // Consumer 2: Sum items\n const consumer2 = stream2.pipe(\n Stream.runFold(0, (acc, n) => acc + n),\n Effect.tap((sum) => Effect.log(`Consumer 2 sum: ${sum}`))\n )\n\n // Consumer 3: Collect to array\n const consumer3 = stream3.pipe(\n Stream.runCollect,\n Effect.tap((items) => Effect.log(`Consumer 3 collected: ${Chunk.toReadonlyArray(items)}`))\n )\n\n // Run all consumers in parallel\n yield* Effect.all([consumer1, consumer2, consumer3], { concurrency: 3 })\n })\n)\n\n// ============================================\n// 2. Partition by predicate\n// ============================================\n\nconst partitionExample = Effect.gen(function* () {\n const numbers = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])\n\n // Partition into even and odd\n const [evens, odds] = yield* Stream.partition(\n numbers,\n (n) => n % 2 === 0\n )\n\n const processEvens = evens.pipe(\n Stream.tap((n) => Effect.log(`Even: ${n}`)),\n Stream.runDrain\n )\n\n const processOdds = odds.pipe(\n Stream.tap((n) => Effect.log(`Odd: ${n}`)),\n Stream.runDrain\n )\n\n yield* Effect.all([processEvens, processOdds], { concurrency: 2 })\n})\n\n// ============================================\n// 3. Partition into multiple buckets\n// ============================================\n\ninterface Event {\n type: \"click\" | \"scroll\" | \"submit\"\n data: unknown\n}\n\nconst multiPartitionExample = Effect.gen(function* () {\n const events: Event[] = [\n { type: \"click\", data: { x: 100 } },\n { type: \"scroll\", data: { y: 200 } },\n { type: \"submit\", data: { form: \"login\" } },\n { type: \"click\", data: { x: 150 } },\n { type: \"scroll\", data: { y: 300 } },\n ]\n\n const source = Stream.fromIterable(events)\n\n // Group by type using groupByKey\n const grouped = source.pipe(\n Stream.groupByKey((event) => event.type, {\n bufferSize: 16,\n })\n )\n\n // Process each group\n yield* grouped.pipe(\n Stream.flatMap(([key, stream]) =>\n stream.pipe(\n Stream.tap((event) => Effect.log(`[${key}] Processing: ${JSON.stringify(event.data)}`)),\n Stream.runDrain,\n Stream.fromEffect\n )\n ),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 4. Fan-out with queues (manual control)\n// ============================================\n\nconst queueFanOut = Effect.gen(function* () {\n const source = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])\n\n // Create queues for each consumer\n const queue1 = yield* Queue.unbounded()\n const queue2 = yield* Queue.unbounded()\n const queue3 = yield* Queue.unbounded()\n\n // Distribute items round-robin\n const distributor = source.pipe(\n Stream.zipWithIndex,\n Stream.tap(([item, index]) => {\n const queue = index % 3 === 0 ? queue1 : index % 3 === 1 ? queue2 : queue3\n return Queue.offer(queue, item)\n }),\n Stream.runDrain,\n Effect.tap(() => Effect.all([\n Queue.shutdown(queue1),\n Queue.shutdown(queue2),\n Queue.shutdown(queue3),\n ]))\n )\n\n // Consumers\n const makeConsumer = (name: string, queue: Queue.Queue) =>\n Stream.fromQueue(queue).pipe(\n Stream.tap((n) => Effect.log(`${name}: ${n}`)),\n Stream.runDrain\n )\n\n yield* Effect.all([\n distributor,\n makeConsumer(\"Worker 1\", queue1),\n makeConsumer(\"Worker 2\", queue2),\n makeConsumer(\"Worker 3\", queue3),\n ], { concurrency: 4 })\n})\n\n// ============================================\n// 5. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Broadcast Example ===\")\n yield* broadcastExample\n\n yield* Effect.log(\"\\n=== Partition Example ===\")\n yield* partitionExample\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Fan-out enables parallel processing:\n\n1. **Throughput** - Multiple consumers process faster\n2. **Specialization** - Different consumers handle different data\n3. **Redundancy** - Multiple copies for reliability\n4. **Decoupling** - Consumers evolve independently\n\n---", - "content": "## Guideline\n\nUse `Stream.broadcast` to send every item to all consumers, or partition streams to distribute items based on criteria.\n\n---\n\n## Rationale\n\nFan-out enables parallel processing:\n\n1. **Throughput** - Multiple consumers process faster\n2. **Specialization** - Different consumers handle different data\n3. **Redundancy** - Multiple copies for reliability\n4. **Decoupling** - Consumers evolve independently\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Queue, Fiber, Chunk } from \"effect\"\n\n// ============================================\n// 1. Broadcast to all consumers\n// ============================================\n\nconst broadcastExample = Effect.scoped(\n Effect.gen(function* () {\n const source = Stream.fromIterable([1, 2, 3, 4, 5])\n\n // Broadcast to 3 consumers - each gets all items\n const [stream1, stream2, stream3] = yield* Stream.broadcast(source, 3)\n\n // Consumer 1: Log items\n const consumer1 = stream1.pipe(\n Stream.tap((n) => Effect.log(`Consumer 1: ${n}`)),\n Stream.runDrain\n )\n\n // Consumer 2: Sum items\n const consumer2 = stream2.pipe(\n Stream.runFold(0, (acc, n) => acc + n),\n Effect.tap((sum) => Effect.log(`Consumer 2 sum: ${sum}`))\n )\n\n // Consumer 3: Collect to array\n const consumer3 = stream3.pipe(\n Stream.runCollect,\n Effect.tap((items) => Effect.log(`Consumer 3 collected: ${Chunk.toReadonlyArray(items)}`))\n )\n\n // Run all consumers in parallel\n yield* Effect.all([consumer1, consumer2, consumer3], { concurrency: 3 })\n })\n)\n\n// ============================================\n// 2. Partition by predicate\n// ============================================\n\nconst partitionExample = Effect.gen(function* () {\n const numbers = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])\n\n // Partition into even and odd\n const [evens, odds] = yield* Stream.partition(\n numbers,\n (n) => n % 2 === 0\n )\n\n const processEvens = evens.pipe(\n Stream.tap((n) => Effect.log(`Even: ${n}`)),\n Stream.runDrain\n )\n\n const processOdds = odds.pipe(\n Stream.tap((n) => Effect.log(`Odd: ${n}`)),\n Stream.runDrain\n )\n\n yield* Effect.all([processEvens, processOdds], { concurrency: 2 })\n})\n\n// ============================================\n// 3. Partition into multiple buckets\n// ============================================\n\ninterface Event {\n type: \"click\" | \"scroll\" | \"submit\"\n data: unknown\n}\n\nconst multiPartitionExample = Effect.gen(function* () {\n const events: Event[] = [\n { type: \"click\", data: { x: 100 } },\n { type: \"scroll\", data: { y: 200 } },\n { type: \"submit\", data: { form: \"login\" } },\n { type: \"click\", data: { x: 150 } },\n { type: \"scroll\", data: { y: 300 } },\n ]\n\n const source = Stream.fromIterable(events)\n\n // Group by type using groupByKey\n const grouped = source.pipe(\n Stream.groupByKey((event) => event.type, {\n bufferSize: 16,\n })\n )\n\n // Process each group\n yield* grouped.pipe(\n Stream.flatMap(([key, stream]) =>\n stream.pipe(\n Stream.tap((event) => Effect.log(`[${key}] Processing: ${JSON.stringify(event.data)}`)),\n Stream.runDrain,\n Stream.fromEffect\n )\n ),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 4. Fan-out with queues (manual control)\n// ============================================\n\nconst queueFanOut = Effect.gen(function* () {\n const source = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])\n\n // Create queues for each consumer\n const queue1 = yield* Queue.unbounded()\n const queue2 = yield* Queue.unbounded()\n const queue3 = yield* Queue.unbounded()\n\n // Distribute items round-robin\n const distributor = source.pipe(\n Stream.zipWithIndex,\n Stream.tap(([item, index]) => {\n const queue = index % 3 === 0 ? queue1 : index % 3 === 1 ? queue2 : queue3\n return Queue.offer(queue, item)\n }),\n Stream.runDrain,\n Effect.tap(() => Effect.all([\n Queue.shutdown(queue1),\n Queue.shutdown(queue2),\n Queue.shutdown(queue3),\n ]))\n )\n\n // Consumers\n const makeConsumer = (name: string, queue: Queue.Queue) =>\n Stream.fromQueue(queue).pipe(\n Stream.tap((n) => Effect.log(`${name}: ${n}`)),\n Stream.runDrain\n )\n\n yield* Effect.all([\n distributor,\n makeConsumer(\"Worker 1\", queue1),\n makeConsumer(\"Worker 2\", queue2),\n makeConsumer(\"Worker 3\", queue3),\n ], { concurrency: 4 })\n})\n\n// ============================================\n// 5. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Broadcast Example ===\")\n yield* broadcastExample\n\n yield* Effect.log(\"\\n=== Partition Example ===\")\n yield* partitionExample\n})\n\nEffect.runPromise(program)\n```\n\n## Fan-Out Patterns\n\n| Pattern | Use Case |\n|---------|----------|\n| **Broadcast** | Every consumer needs all data |\n| **Partition** | Route by predicate |\n| **GroupBy** | Route by key |\n| **Round-robin** | Load balancing |\n\n## Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `Stream.broadcast` | Clone stream to N consumers |\n| `Stream.partition` | Split by predicate |\n| `Stream.groupByKey` | Group by key function |\n| `Stream.distributedWith` | Custom distribution |\n\n## Best Practices\n\n1. **Bound queues** - Prevent memory issues\n2. **Handle slow consumers** - Don't block fast ones\n3. **Monitor lag** - Track consumer progress\n4. **Graceful shutdown** - Complete in-flight items" - }, - { - "id": "combinator-filter", - "title": "Filtering Results with filter", - "description": "Use filter to declaratively express conditional logic, keeping only values that satisfy a predicate.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Only succeed if the value is even, fail otherwise\nconst effect = Effect.succeed(4).pipe(\n Effect.filterOrFail(\n (n): n is number => n % 2 === 0,\n () => \"Number is not even\"\n )\n); // Effect\n\n// Option: Only keep the value if it is even\nconst option = Option.some(4).pipe(\n Option.filter((n): n is number => n % 2 === 0)\n); // Option\n\n// Either: Use map and flatMap to filter\nconst either = Either.right(4).pipe(\n Either.flatMap((n) =>\n n % 2 === 0 ? Either.right(n) : Either.left(\"Number is not even\")\n )\n); // Either\n\n// Stream: Only emit even numbers\nconst stream = Stream.fromIterable([1, 2, 3, 4]).pipe(\n Stream.filter((n): n is number => n % 2 === 0)\n); // Stream\n```\n\n**Explanation:** \n`filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Either.left`), `Option.none`, or an empty stream.", - "antiPattern": "Using `map` with a conditional that returns `Option` or `Either`, then manually flattening, instead of using `filter`. \nThis leads to unnecessary complexity and less readable code.", - "explanation": "`filter` lets you express \"only continue if...\" logic without resorting to manual checks or imperative branching. \nIt keeps your code composable and type-safe, and ensures that failures or empty results are handled consistently.", - "content": "# Filtering Results with `filter`\n\n## Guideline\n\nUse the `filter` combinator to keep only those values that satisfy a predicate. \nThis works for `Effect`, `Stream`, `Option`, and `Either`, allowing you to express conditional logic declaratively and safely.\n\n## Rationale\n\n`filter` lets you express \"only continue if...\" logic without resorting to manual checks or imperative branching. \nIt keeps your code composable and type-safe, and ensures that failures or empty results are handled consistently.\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Only succeed if the value is even, fail otherwise\nconst effect = Effect.succeed(4).pipe(\n Effect.filterOrFail(\n (n): n is number => n % 2 === 0,\n () => \"Number is not even\"\n )\n); // Effect\n\n// Option: Only keep the value if it is even\nconst option = Option.some(4).pipe(\n Option.filter((n): n is number => n % 2 === 0)\n); // Option\n\n// Either: Use map and flatMap to filter\nconst either = Either.right(4).pipe(\n Either.flatMap((n) =>\n n % 2 === 0 ? Either.right(n) : Either.left(\"Number is not even\")\n )\n); // Either\n\n// Stream: Only emit even numbers\nconst stream = Stream.fromIterable([1, 2, 3, 4]).pipe(\n Stream.filter((n): n is number => n % 2 === 0)\n); // Stream\n```\n\n**Explanation:** \n`filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Either.left`), `Option.none`, or an empty stream.\n\n## Anti-Pattern\n\nUsing `map` with a conditional that returns `Option` or `Either`, then manually flattening, instead of using `filter`. \nThis leads to unnecessary complexity and less readable code." - }, - { - "id": "api-openapi", - "title": "Generate OpenAPI Documentation", - "description": "Use Schema definitions to automatically generate OpenAPI documentation for your API.", - "skillLevel": "advanced", - "useCases": [ - "building-apis" - ], - "example": "```typescript\nimport { Effect, Schema } from \"effect\"\nimport {\n HttpApi,\n HttpApiBuilder,\n HttpApiEndpoint,\n HttpApiGroup,\n HttpApiSwagger,\n OpenApi,\n} from \"@effect/platform\"\n\n// ============================================\n// 1. Define schemas for request/response\n// ============================================\n\nconst UserSchema = Schema.Struct({\n id: Schema.String,\n email: Schema.String.pipe(Schema.pattern(/@/)),\n name: Schema.String,\n createdAt: Schema.DateFromString,\n})\n\nconst CreateUserSchema = Schema.Struct({\n email: Schema.String.pipe(Schema.pattern(/@/)),\n name: Schema.String,\n})\n\nconst UserListSchema = Schema.Array(UserSchema)\n\nconst ErrorSchema = Schema.Struct({\n error: Schema.String,\n code: Schema.String,\n})\n\n// ============================================\n// 2. Define API endpoints with schemas\n// ============================================\n\nconst usersApi = HttpApiGroup.make(\"users\")\n .pipe(\n HttpApiGroup.add(\n HttpApiEndpoint.get(\"getUsers\", \"/users\")\n .pipe(\n HttpApiEndpoint.setSuccess(UserListSchema),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.get(\"getUser\", \"/users/:id\")\n .pipe(\n HttpApiEndpoint.setPath(Schema.Struct({\n id: Schema.String,\n })),\n HttpApiEndpoint.setSuccess(UserSchema),\n HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.post(\"createUser\", \"/users\")\n .pipe(\n HttpApiEndpoint.setPayload(CreateUserSchema),\n HttpApiEndpoint.setSuccess(UserSchema, { status: 201 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 400 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.del(\"deleteUser\", \"/users/:id\")\n .pipe(\n HttpApiEndpoint.setPath(Schema.Struct({\n id: Schema.String,\n })),\n HttpApiEndpoint.setSuccess(Schema.Void, { status: 204 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n )\n )\n\n// ============================================\n// 3. Create the API definition\n// ============================================\n\nconst api = HttpApi.make(\"My API\")\n .pipe(\n HttpApi.addGroup(usersApi),\n OpenApi.annotate({\n title: \"My Effect API\",\n version: \"1.0.0\",\n description: \"A sample API built with Effect\",\n })\n )\n\n// ============================================\n// 4. Implement the handlers\n// ============================================\n\nconst usersHandlers = HttpApiBuilder.group(api, \"users\", (handlers) =>\n handlers\n .pipe(\n HttpApiBuilder.handle(\"getUsers\", () =>\n Effect.succeed([\n {\n id: \"1\",\n email: \"alice@example.com\",\n name: \"Alice\",\n createdAt: new Date(),\n },\n ])\n ),\n HttpApiBuilder.handle(\"getUser\", ({ path }) =>\n Effect.gen(function* () {\n if (path.id === \"not-found\") {\n return yield* Effect.fail({ error: \"User not found\", code: \"NOT_FOUND\" })\n }\n return {\n id: path.id,\n email: \"user@example.com\",\n name: \"User\",\n createdAt: new Date(),\n }\n })\n ),\n HttpApiBuilder.handle(\"createUser\", ({ payload }) =>\n Effect.succeed({\n id: crypto.randomUUID(),\n email: payload.email,\n name: payload.name,\n createdAt: new Date(),\n })\n ),\n HttpApiBuilder.handle(\"deleteUser\", ({ path }) =>\n Effect.gen(function* () {\n if (path.id === \"not-found\") {\n return yield* Effect.fail({ error: \"User not found\", code: \"NOT_FOUND\" })\n }\n yield* Effect.log(`Deleted user ${path.id}`)\n })\n )\n )\n)\n\n// ============================================\n// 5. Build the server with Swagger UI\n// ============================================\n\nconst MyApiLive = HttpApiBuilder.api(api).pipe(\n Layer.provide(usersHandlers)\n)\n\nconst ServerLive = HttpApiBuilder.serve().pipe(\n // Add Swagger UI at /docs\n Layer.provide(HttpApiSwagger.layer({ path: \"/docs\" })),\n Layer.provide(MyApiLive),\n Layer.provide(NodeHttpServer.layer({ port: 3000 }))\n)\n\n// ============================================\n// 6. Export OpenAPI spec as JSON\n// ============================================\n\nconst openApiSpec = OpenApi.fromApi(api)\n\n// Save to file for external tools\nimport { NodeFileSystem } from \"@effect/platform-node\"\n\nconst saveSpec = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n yield* fs.writeFileString(\n \"openapi.json\",\n JSON.stringify(openApiSpec, null, 2)\n )\n yield* Effect.log(\"OpenAPI spec saved to openapi.json\")\n})\n```", - "antiPattern": "", - "explanation": "OpenAPI documentation provides:\n\n1. **Discovery** - Clients know what endpoints exist\n2. **Contracts** - Clear request/response shapes\n3. **Testing** - Swagger UI for manual testing\n4. **Code generation** - Generate client SDKs\n5. **Validation** - Schema-first development\n\n---", - "content": "## Guideline\n\nDefine your API using Effect Schema and HttpApi to automatically generate OpenAPI documentation that stays in sync with your implementation.\n\n---\n\n## Rationale\n\nOpenAPI documentation provides:\n\n1. **Discovery** - Clients know what endpoints exist\n2. **Contracts** - Clear request/response shapes\n3. **Testing** - Swagger UI for manual testing\n4. **Code generation** - Generate client SDKs\n5. **Validation** - Schema-first development\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Schema } from \"effect\"\nimport {\n HttpApi,\n HttpApiBuilder,\n HttpApiEndpoint,\n HttpApiGroup,\n HttpApiSwagger,\n OpenApi,\n} from \"@effect/platform\"\n\n// ============================================\n// 1. Define schemas for request/response\n// ============================================\n\nconst UserSchema = Schema.Struct({\n id: Schema.String,\n email: Schema.String.pipe(Schema.pattern(/@/)),\n name: Schema.String,\n createdAt: Schema.DateFromString,\n})\n\nconst CreateUserSchema = Schema.Struct({\n email: Schema.String.pipe(Schema.pattern(/@/)),\n name: Schema.String,\n})\n\nconst UserListSchema = Schema.Array(UserSchema)\n\nconst ErrorSchema = Schema.Struct({\n error: Schema.String,\n code: Schema.String,\n})\n\n// ============================================\n// 2. Define API endpoints with schemas\n// ============================================\n\nconst usersApi = HttpApiGroup.make(\"users\")\n .pipe(\n HttpApiGroup.add(\n HttpApiEndpoint.get(\"getUsers\", \"/users\")\n .pipe(\n HttpApiEndpoint.setSuccess(UserListSchema),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.get(\"getUser\", \"/users/:id\")\n .pipe(\n HttpApiEndpoint.setPath(Schema.Struct({\n id: Schema.String,\n })),\n HttpApiEndpoint.setSuccess(UserSchema),\n HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.post(\"createUser\", \"/users\")\n .pipe(\n HttpApiEndpoint.setPayload(CreateUserSchema),\n HttpApiEndpoint.setSuccess(UserSchema, { status: 201 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 400 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n ),\n HttpApiGroup.add(\n HttpApiEndpoint.del(\"deleteUser\", \"/users/:id\")\n .pipe(\n HttpApiEndpoint.setPath(Schema.Struct({\n id: Schema.String,\n })),\n HttpApiEndpoint.setSuccess(Schema.Void, { status: 204 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 404 }),\n HttpApiEndpoint.addError(ErrorSchema, { status: 500 })\n )\n )\n )\n\n// ============================================\n// 3. Create the API definition\n// ============================================\n\nconst api = HttpApi.make(\"My API\")\n .pipe(\n HttpApi.addGroup(usersApi),\n OpenApi.annotate({\n title: \"My Effect API\",\n version: \"1.0.0\",\n description: \"A sample API built with Effect\",\n })\n )\n\n// ============================================\n// 4. Implement the handlers\n// ============================================\n\nconst usersHandlers = HttpApiBuilder.group(api, \"users\", (handlers) =>\n handlers\n .pipe(\n HttpApiBuilder.handle(\"getUsers\", () =>\n Effect.succeed([\n {\n id: \"1\",\n email: \"alice@example.com\",\n name: \"Alice\",\n createdAt: new Date(),\n },\n ])\n ),\n HttpApiBuilder.handle(\"getUser\", ({ path }) =>\n Effect.gen(function* () {\n if (path.id === \"not-found\") {\n return yield* Effect.fail({ error: \"User not found\", code: \"NOT_FOUND\" })\n }\n return {\n id: path.id,\n email: \"user@example.com\",\n name: \"User\",\n createdAt: new Date(),\n }\n })\n ),\n HttpApiBuilder.handle(\"createUser\", ({ payload }) =>\n Effect.succeed({\n id: crypto.randomUUID(),\n email: payload.email,\n name: payload.name,\n createdAt: new Date(),\n })\n ),\n HttpApiBuilder.handle(\"deleteUser\", ({ path }) =>\n Effect.gen(function* () {\n if (path.id === \"not-found\") {\n return yield* Effect.fail({ error: \"User not found\", code: \"NOT_FOUND\" })\n }\n yield* Effect.log(`Deleted user ${path.id}`)\n })\n )\n )\n)\n\n// ============================================\n// 5. Build the server with Swagger UI\n// ============================================\n\nconst MyApiLive = HttpApiBuilder.api(api).pipe(\n Layer.provide(usersHandlers)\n)\n\nconst ServerLive = HttpApiBuilder.serve().pipe(\n // Add Swagger UI at /docs\n Layer.provide(HttpApiSwagger.layer({ path: \"/docs\" })),\n Layer.provide(MyApiLive),\n Layer.provide(NodeHttpServer.layer({ port: 3000 }))\n)\n\n// ============================================\n// 6. Export OpenAPI spec as JSON\n// ============================================\n\nconst openApiSpec = OpenApi.fromApi(api)\n\n// Save to file for external tools\nimport { NodeFileSystem } from \"@effect/platform-node\"\n\nconst saveSpec = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n yield* fs.writeFileString(\n \"openapi.json\",\n JSON.stringify(openApiSpec, null, 2)\n )\n yield* Effect.log(\"OpenAPI spec saved to openapi.json\")\n})\n```\n\n## Generated OpenAPI Structure\n\n```yaml\nopenapi: 3.0.0\ninfo:\n title: My Effect API\n version: 1.0.0\n description: A sample API built with Effect\npaths:\n /users:\n get:\n operationId: getUsers\n responses:\n 200:\n content:\n application/json:\n schema:\n type: array\n items:\n $ref: '#/components/schemas/User'\n post:\n operationId: createUser\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/CreateUser'\n responses:\n 201:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/User'\n```\n\n## Key Benefits\n\n| Benefit | How |\n|---------|-----|\n| **Type-safe** | Schemas used at runtime and in docs |\n| **Auto-sync** | Change code, docs update |\n| **Swagger UI** | Built-in interactive docs |\n| **Validation** | Request validation from schemas |\n\n## Best Practices\n\n1. **Schema-first** - Define schemas before handlers\n2. **Document errors** - Include all error responses\n3. **Use annotations** - Add descriptions, examples\n4. **Version your API** - Track breaking changes\n5. **Export spec** - Generate for client SDK generation" - }, - { - "id": "handle-get-request", - "title": "Handle a GET Request", - "description": "Use Http.router.get to associate a URL path with a specific response Effect.", - "skillLevel": "beginner", - "useCases": [ - "building-apis" - ], - "example": "This example defines two separate GET routes, one for the root path (`/`) and one for `/hello`. We create an empty router and add each route to it. The resulting `app` is then served. The router automatically handles sending a `404 Not Found` response for any path that doesn't match.\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define response types\ninterface RouteResponse {\n readonly status: number;\n readonly body: string;\n}\n\n// Define error types\nclass RouteNotFoundError extends Data.TaggedError(\"RouteNotFoundError\")<{\n readonly path: string;\n}> {}\n\nclass RouteHandlerError extends Data.TaggedError(\"RouteHandlerError\")<{\n readonly path: string;\n readonly error: string;\n}> {}\n\n// Define route service\nclass RouteService extends Effect.Service()(\"RouteService\", {\n sync: () => {\n // Create instance methods\n const handleRoute = (\n path: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Processing request for path: ${path}`);\n\n try {\n switch (path) {\n case \"/\":\n const home = \"Welcome to the home page!\";\n yield* Effect.logInfo(`Serving home page`);\n return { status: 200, body: home };\n\n case \"/hello\":\n const hello = \"Hello, Effect!\";\n yield* Effect.logInfo(`Serving hello page`);\n return { status: 200, body: hello };\n\n default:\n yield* Effect.logWarning(`Route not found: ${path}`);\n return yield* Effect.fail(new RouteNotFoundError({ path }));\n }\n } catch (e) {\n const error = e instanceof Error ? e.message : String(e);\n yield* Effect.logError(`Error handling route ${path}: ${error}`);\n return yield* Effect.fail(new RouteHandlerError({ path, error }));\n }\n });\n\n // Return service implementation\n return {\n handleRoute,\n // Simulate GET request\n simulateGet: (\n path: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`GET ${path}`);\n const response = yield* handleRoute(path);\n yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);\n return response;\n }),\n };\n },\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const router = yield* RouteService;\n\n yield* Effect.logInfo(\"=== Starting Route Tests ===\");\n\n // Test different routes\n for (const path of [\"/\", \"/hello\", \"/other\", \"/error\"]) {\n yield* Effect.logInfo(`\\n--- Testing ${path} ---`);\n\n const result = yield* router.simulateGet(path).pipe(\n Effect.catchTags({\n RouteNotFoundError: (error) =>\n Effect.gen(function* () {\n const response = { status: 404, body: `Not Found: ${error.path}` };\n yield* Effect.logWarning(`${response.status} ${response.body}`);\n return response;\n }),\n RouteHandlerError: (error) =>\n Effect.gen(function* () {\n const response = {\n status: 500,\n body: `Internal Error: ${error.error}`,\n };\n yield* Effect.logError(`${response.status} ${response.body}`);\n return response;\n }),\n })\n );\n\n yield* Effect.logInfo(`Final Response: ${JSON.stringify(result)}`);\n }\n\n yield* Effect.logInfo(\"\\n=== Route Tests Complete ===\");\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, RouteService.Default));\n```", - "antiPattern": "The anti-pattern is to create a single, monolithic handler that uses conditional logic to inspect the request URL. This imperative approach is difficult to maintain and scale.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// A single app that manually checks the URL\nconst app = Http.request.ServerRequest.pipe(\n Effect.flatMap((req) => {\n if (req.url === \"/\") {\n return Effect.succeed(Http.response.text(\"Welcome to the home page!\"));\n } else if (req.url === \"/hello\") {\n return Effect.succeed(Http.response.text(\"Hello, Effect!\"));\n } else {\n return Effect.succeed(Http.response.empty({ status: 404 }));\n }\n })\n);\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the \"what\" (the response) with the \"where\" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `Http.router` is designed to solve all of these problems elegantly.", - "explanation": "A real application needs to respond differently to different URLs. The `Http.router` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods.\n\nThis approach has several advantages:\n\n1. **Declarative and Readable**: Your code clearly expresses the mapping between a URL path and its behavior, making the application's structure easy to understand.\n2. **Composability**: Routers are just values that can be created, combined, and passed around. This makes it easy to organize routes into logical groups (e.g., a `userRoutes` router and a `productRoutes` router) and merge them.\n3. **Type Safety**: The router ensures that the handler for a route is only ever called for a matching request, simplifying the logic within the handler itself.\n4. **Integration**: Each route handler is an `Effect`, meaning it has full access to dependency injection, structured concurrency, and integrated error handling, just like any other part of an Effect application.\n\n---", - "content": "## Guideline\n\nTo handle specific URL paths, create individual routes using `Http.router` functions (like `Http.router.get`) and combine them into a single `Http.App`.\n\n---\n\n## Rationale\n\nA real application needs to respond differently to different URLs. The `Http.router` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods.\n\nThis approach has several advantages:\n\n1. **Declarative and Readable**: Your code clearly expresses the mapping between a URL path and its behavior, making the application's structure easy to understand.\n2. **Composability**: Routers are just values that can be created, combined, and passed around. This makes it easy to organize routes into logical groups (e.g., a `userRoutes` router and a `productRoutes` router) and merge them.\n3. **Type Safety**: The router ensures that the handler for a route is only ever called for a matching request, simplifying the logic within the handler itself.\n4. **Integration**: Each route handler is an `Effect`, meaning it has full access to dependency injection, structured concurrency, and integrated error handling, just like any other part of an Effect application.\n\n---\n\n## Good Example\n\nThis example defines two separate GET routes, one for the root path (`/`) and one for `/hello`. We create an empty router and add each route to it. The resulting `app` is then served. The router automatically handles sending a `404 Not Found` response for any path that doesn't match.\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define response types\ninterface RouteResponse {\n readonly status: number;\n readonly body: string;\n}\n\n// Define error types\nclass RouteNotFoundError extends Data.TaggedError(\"RouteNotFoundError\")<{\n readonly path: string;\n}> {}\n\nclass RouteHandlerError extends Data.TaggedError(\"RouteHandlerError\")<{\n readonly path: string;\n readonly error: string;\n}> {}\n\n// Define route service\nclass RouteService extends Effect.Service()(\"RouteService\", {\n sync: () => {\n // Create instance methods\n const handleRoute = (\n path: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Processing request for path: ${path}`);\n\n try {\n switch (path) {\n case \"/\":\n const home = \"Welcome to the home page!\";\n yield* Effect.logInfo(`Serving home page`);\n return { status: 200, body: home };\n\n case \"/hello\":\n const hello = \"Hello, Effect!\";\n yield* Effect.logInfo(`Serving hello page`);\n return { status: 200, body: hello };\n\n default:\n yield* Effect.logWarning(`Route not found: ${path}`);\n return yield* Effect.fail(new RouteNotFoundError({ path }));\n }\n } catch (e) {\n const error = e instanceof Error ? e.message : String(e);\n yield* Effect.logError(`Error handling route ${path}: ${error}`);\n return yield* Effect.fail(new RouteHandlerError({ path, error }));\n }\n });\n\n // Return service implementation\n return {\n handleRoute,\n // Simulate GET request\n simulateGet: (\n path: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`GET ${path}`);\n const response = yield* handleRoute(path);\n yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);\n return response;\n }),\n };\n },\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const router = yield* RouteService;\n\n yield* Effect.logInfo(\"=== Starting Route Tests ===\");\n\n // Test different routes\n for (const path of [\"/\", \"/hello\", \"/other\", \"/error\"]) {\n yield* Effect.logInfo(`\\n--- Testing ${path} ---`);\n\n const result = yield* router.simulateGet(path).pipe(\n Effect.catchTags({\n RouteNotFoundError: (error) =>\n Effect.gen(function* () {\n const response = { status: 404, body: `Not Found: ${error.path}` };\n yield* Effect.logWarning(`${response.status} ${response.body}`);\n return response;\n }),\n RouteHandlerError: (error) =>\n Effect.gen(function* () {\n const response = {\n status: 500,\n body: `Internal Error: ${error.error}`,\n };\n yield* Effect.logError(`${response.status} ${response.body}`);\n return response;\n }),\n })\n );\n\n yield* Effect.logInfo(`Final Response: ${JSON.stringify(result)}`);\n }\n\n yield* Effect.logInfo(\"\\n=== Route Tests Complete ===\");\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, RouteService.Default));\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to create a single, monolithic handler that uses conditional logic to inspect the request URL. This imperative approach is difficult to maintain and scale.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// A single app that manually checks the URL\nconst app = Http.request.ServerRequest.pipe(\n Effect.flatMap((req) => {\n if (req.url === \"/\") {\n return Effect.succeed(Http.response.text(\"Welcome to the home page!\"));\n } else if (req.url === \"/hello\") {\n return Effect.succeed(Http.response.text(\"Hello, Effect!\"));\n } else {\n return Effect.succeed(Http.response.empty({ status: 404 }));\n }\n })\n);\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the \"what\" (the response) with the \"where\" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `Http.router` is designed to solve all of these problems elegantly." - }, - { - "id": "handle-api-errors", - "title": "Handle API Errors", - "description": "Model application errors as typed classes and use Http.server.serveOptions to map them to specific HTTP responses.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "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.\n\n```typescript\nimport { Cause, Data, Effect } from \"effect\";\n\n// Define our domain types\nexport interface User {\n readonly id: string;\n readonly name: string;\n readonly email: string;\n readonly role: \"admin\" | \"user\";\n}\n\n// Define specific, typed errors for our domain\nexport class UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n readonly id: string;\n}> {}\n\nexport class InvalidIdError extends Data.TaggedError(\"InvalidIdError\")<{\n readonly id: string;\n readonly reason: string;\n}> {}\n\nexport class UnauthorizedError extends Data.TaggedError(\"UnauthorizedError\")<{\n readonly action: string;\n readonly role: string;\n}> {}\n\n// Define error handler service\nexport class ErrorHandlerService extends Effect.Service()(\n \"ErrorHandlerService\",\n {\n sync: () => ({\n // Handle API errors with proper logging\n handleApiError: (error: E): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logError(`API Error: ${JSON.stringify(error)}`);\n\n if (error instanceof UserNotFoundError) {\n return {\n error: \"Not Found\",\n message: `User ${error.id} not found`,\n };\n }\n if (error instanceof InvalidIdError) {\n return { error: \"Bad Request\", message: error.reason };\n }\n if (error instanceof UnauthorizedError) {\n return {\n error: \"Unauthorized\",\n message: `${error.role} cannot ${error.action}`,\n };\n }\n\n return {\n error: \"Internal Server Error\",\n message: \"An unexpected error occurred\",\n };\n }),\n\n // Handle unexpected errors\n handleUnexpectedError: (\n cause: Cause.Cause\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logError(\"Unexpected error occurred\");\n\n if (Cause.isDie(cause)) {\n const defect = Cause.failureOption(cause);\n if (defect._tag === \"Some\") {\n const error = defect.value as Error;\n yield* Effect.logError(`Defect: ${error.message}`);\n yield* Effect.logError(\n `Stack: ${error.stack?.split(\"\\n\")[1]?.trim() ?? \"N/A\"}`\n );\n }\n }\n\n return Effect.succeed(void 0);\n }),\n }),\n }\n) {}\n\n// Define UserRepository service\nexport class UserRepository extends Effect.Service()(\n \"UserRepository\",\n {\n sync: () => {\n const users = new Map([\n [\n \"user_123\",\n {\n id: \"user_123\",\n name: \"Paul\",\n email: \"paul@example.com\",\n role: \"admin\",\n },\n ],\n [\n \"user_456\",\n {\n id: \"user_456\",\n name: \"Alice\",\n email: \"alice@example.com\",\n role: \"user\",\n },\n ],\n ]);\n\n return {\n // Get user by ID with proper error handling\n getUser: (\n id: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Attempting to get user with id: ${id}`);\n\n // Validate ID format\n if (!id.match(/^user_\\d+$/)) {\n yield* Effect.logWarning(`Invalid user ID format: ${id}`);\n return yield* Effect.fail(\n new InvalidIdError({\n id,\n reason: \"ID must be in format user_\",\n })\n );\n }\n\n const user = users.get(id);\n if (user === undefined) {\n yield* Effect.logWarning(`User not found with id: ${id}`);\n return yield* Effect.fail(new UserNotFoundError({ id }));\n }\n\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n return user;\n }),\n\n // Check if user has required role\n checkRole: (\n user: User,\n requiredRole: \"admin\" | \"user\"\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\n `Checking if user ${user.id} has role: ${requiredRole}`\n );\n\n if (user.role !== requiredRole && user.role !== \"admin\") {\n yield* Effect.logWarning(\n `User ${user.id} with role ${user.role} cannot access ${requiredRole} resources`\n );\n return yield* Effect.fail(\n new UnauthorizedError({\n action: \"access_user\",\n role: user.role,\n })\n );\n }\n\n yield* Effect.logInfo(\n `User ${user.id} has required role: ${user.role}`\n );\n return Effect.succeed(void 0);\n }),\n };\n },\n }\n) {}\n\ninterface ApiResponse {\n readonly error?: string;\n readonly message?: string;\n readonly data?: User;\n}\n\n// Create routes with proper error handling\nconst createRoutes = () =>\n Effect.gen(function* () {\n const repo = yield* UserRepository;\n const errorHandler = yield* ErrorHandlerService;\n\n yield* Effect.logInfo(\"=== Processing API request ===\");\n\n // Test different scenarios\n for (const userId of [\"user_123\", \"user_456\", \"invalid_id\", \"user_789\"]) {\n yield* Effect.logInfo(`\\n--- Testing user ID: ${userId} ---`);\n\n const response = yield* repo.getUser(userId).pipe(\n Effect.map((user) => ({\n data: {\n ...user,\n email: user.role === \"admin\" ? user.email : \"[hidden]\",\n },\n })),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);\n }\n\n // Test role checking\n const adminUser = yield* repo.getUser(\"user_123\");\n const regularUser = yield* repo.getUser(\"user_456\");\n\n yield* Effect.logInfo(\"\\n=== Testing role checks ===\");\n\n yield* repo.checkRole(adminUser, \"admin\").pipe(\n Effect.tap(() => Effect.logInfo(\"Admin access successful\")),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n yield* repo.checkRole(regularUser, \"admin\").pipe(\n Effect.tap(() => Effect.logInfo(\"User admin access successful\")),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n return { message: \"Tests completed successfully\" };\n });\n\n// Run the program with all services\nEffect.runPromise(\n Effect.provide(\n Effect.provide(createRoutes(), ErrorHandlerService.Default),\n UserRepository.Default\n )\n);\n```", - "antiPattern": "The anti-pattern is to handle HTTP-specific error logic inside each route handler using functions like `Effect.catchTag`.\n\n```typescript\nimport { Effect, Data } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n// ... same getUser function and error classes\n\nconst userRoute = Http.router.get(\n \"/users/:userId\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n getUser(req.params.userId)\n ).pipe(\n Effect.map(Http.response.json),\n // Manually catching errors inside the route logic\n Effect.catchTag(\"UserNotFoundError\", (e) =>\n Http.response.text(`User ${e.id} not found`, { status: 404 })\n ),\n Effect.catchTag(\"InvalidIdError\", (e) =>\n Http.response.text(`ID ${e.id} is not a valid format`, { status: 400 })\n )\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(userRoute));\n\n// No centralized error handling\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis approach is problematic because it pollutes the business logic of the route handler with details about HTTP status codes. It's also highly repetitive; if ten different routes could produce a `UserNotFoundError`, you would need to copy this `catchTag` logic into all ten of them, making the API difficult to maintain.", - "explanation": "By default, any unhandled failure in an Effect route handler results in a generic `500 Internal Server Error`. This is a safe default, but it's not helpful for API clients who need to know _why_ their request failed. Was it a client-side error (like a non-existent resource, `404`) or a true server-side problem (`500`)?\n\nCentralizing error handling at the server level provides a clean separation of concerns:\n\n1. **Domain-Focused Logic**: Your business logic can fail with specific, descriptive errors (e.g., `UserNotFoundError`) without needing any knowledge of HTTP status codes.\n2. **Centralized Mapping**: You define the mapping from application errors to HTTP responses in a single location. This makes your API's error handling consistent and easy to maintain. If you need to change how an error is reported, you only change it in one place.\n3. **Type Safety**: Using `Data.TaggedClass` for your errors allows you to use `Match` to exhaustively handle all known error cases, preventing you from forgetting to map a specific error type.\n4. **Clear Client Communication**: It produces a predictable and useful API, allowing clients to programmatically react to different failure scenarios.\n\n---", - "content": "## Guideline\n\nDefine specific error types for your application logic and use `Http.server.serveOptions` with a custom `unhandledErrorResponse` function to map those errors to appropriate HTTP status codes and responses.\n\n---\n\n## Rationale\n\nBy default, any unhandled failure in an Effect route handler results in a generic `500 Internal Server Error`. This is a safe default, but it's not helpful for API clients who need to know _why_ their request failed. Was it a client-side error (like a non-existent resource, `404`) or a true server-side problem (`500`)?\n\nCentralizing error handling at the server level provides a clean separation of concerns:\n\n1. **Domain-Focused Logic**: Your business logic can fail with specific, descriptive errors (e.g., `UserNotFoundError`) without needing any knowledge of HTTP status codes.\n2. **Centralized Mapping**: You define the mapping from application errors to HTTP responses in a single location. This makes your API's error handling consistent and easy to maintain. If you need to change how an error is reported, you only change it in one place.\n3. **Type Safety**: Using `Data.TaggedClass` for your errors allows you to use `Match` to exhaustively handle all known error cases, preventing you from forgetting to map a specific error type.\n4. **Clear Client Communication**: It produces a predictable and useful API, allowing clients to programmatically react to different failure scenarios.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Cause, Data, Effect } from \"effect\";\n\n// Define our domain types\nexport interface User {\n readonly id: string;\n readonly name: string;\n readonly email: string;\n readonly role: \"admin\" | \"user\";\n}\n\n// Define specific, typed errors for our domain\nexport class UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n readonly id: string;\n}> {}\n\nexport class InvalidIdError extends Data.TaggedError(\"InvalidIdError\")<{\n readonly id: string;\n readonly reason: string;\n}> {}\n\nexport class UnauthorizedError extends Data.TaggedError(\"UnauthorizedError\")<{\n readonly action: string;\n readonly role: string;\n}> {}\n\n// Define error handler service\nexport class ErrorHandlerService extends Effect.Service()(\n \"ErrorHandlerService\",\n {\n sync: () => ({\n // Handle API errors with proper logging\n handleApiError: (error: E): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logError(`API Error: ${JSON.stringify(error)}`);\n\n if (error instanceof UserNotFoundError) {\n return {\n error: \"Not Found\",\n message: `User ${error.id} not found`,\n };\n }\n if (error instanceof InvalidIdError) {\n return { error: \"Bad Request\", message: error.reason };\n }\n if (error instanceof UnauthorizedError) {\n return {\n error: \"Unauthorized\",\n message: `${error.role} cannot ${error.action}`,\n };\n }\n\n return {\n error: \"Internal Server Error\",\n message: \"An unexpected error occurred\",\n };\n }),\n\n // Handle unexpected errors\n handleUnexpectedError: (\n cause: Cause.Cause\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logError(\"Unexpected error occurred\");\n\n if (Cause.isDie(cause)) {\n const defect = Cause.failureOption(cause);\n if (defect._tag === \"Some\") {\n const error = defect.value as Error;\n yield* Effect.logError(`Defect: ${error.message}`);\n yield* Effect.logError(\n `Stack: ${error.stack?.split(\"\\n\")[1]?.trim() ?? \"N/A\"}`\n );\n }\n }\n\n return Effect.succeed(void 0);\n }),\n }),\n }\n) {}\n\n// Define UserRepository service\nexport class UserRepository extends Effect.Service()(\n \"UserRepository\",\n {\n sync: () => {\n const users = new Map([\n [\n \"user_123\",\n {\n id: \"user_123\",\n name: \"Paul\",\n email: \"paul@example.com\",\n role: \"admin\",\n },\n ],\n [\n \"user_456\",\n {\n id: \"user_456\",\n name: \"Alice\",\n email: \"alice@example.com\",\n role: \"user\",\n },\n ],\n ]);\n\n return {\n // Get user by ID with proper error handling\n getUser: (\n id: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Attempting to get user with id: ${id}`);\n\n // Validate ID format\n if (!id.match(/^user_\\d+$/)) {\n yield* Effect.logWarning(`Invalid user ID format: ${id}`);\n return yield* Effect.fail(\n new InvalidIdError({\n id,\n reason: \"ID must be in format user_\",\n })\n );\n }\n\n const user = users.get(id);\n if (user === undefined) {\n yield* Effect.logWarning(`User not found with id: ${id}`);\n return yield* Effect.fail(new UserNotFoundError({ id }));\n }\n\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n return user;\n }),\n\n // Check if user has required role\n checkRole: (\n user: User,\n requiredRole: \"admin\" | \"user\"\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\n `Checking if user ${user.id} has role: ${requiredRole}`\n );\n\n if (user.role !== requiredRole && user.role !== \"admin\") {\n yield* Effect.logWarning(\n `User ${user.id} with role ${user.role} cannot access ${requiredRole} resources`\n );\n return yield* Effect.fail(\n new UnauthorizedError({\n action: \"access_user\",\n role: user.role,\n })\n );\n }\n\n yield* Effect.logInfo(\n `User ${user.id} has required role: ${user.role}`\n );\n return Effect.succeed(void 0);\n }),\n };\n },\n }\n) {}\n\ninterface ApiResponse {\n readonly error?: string;\n readonly message?: string;\n readonly data?: User;\n}\n\n// Create routes with proper error handling\nconst createRoutes = () =>\n Effect.gen(function* () {\n const repo = yield* UserRepository;\n const errorHandler = yield* ErrorHandlerService;\n\n yield* Effect.logInfo(\"=== Processing API request ===\");\n\n // Test different scenarios\n for (const userId of [\"user_123\", \"user_456\", \"invalid_id\", \"user_789\"]) {\n yield* Effect.logInfo(`\\n--- Testing user ID: ${userId} ---`);\n\n const response = yield* repo.getUser(userId).pipe(\n Effect.map((user) => ({\n data: {\n ...user,\n email: user.role === \"admin\" ? user.email : \"[hidden]\",\n },\n })),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`);\n }\n\n // Test role checking\n const adminUser = yield* repo.getUser(\"user_123\");\n const regularUser = yield* repo.getUser(\"user_456\");\n\n yield* Effect.logInfo(\"\\n=== Testing role checks ===\");\n\n yield* repo.checkRole(adminUser, \"admin\").pipe(\n Effect.tap(() => Effect.logInfo(\"Admin access successful\")),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n yield* repo.checkRole(regularUser, \"admin\").pipe(\n Effect.tap(() => Effect.logInfo(\"User admin access successful\")),\n Effect.catchAll((error) => errorHandler.handleApiError(error))\n );\n\n return { message: \"Tests completed successfully\" };\n });\n\n// Run the program with all services\nEffect.runPromise(\n Effect.provide(\n Effect.provide(createRoutes(), ErrorHandlerService.Default),\n UserRepository.Default\n )\n);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to handle HTTP-specific error logic inside each route handler using functions like `Effect.catchTag`.\n\n```typescript\nimport { Effect, Data } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n// ... same getUser function and error classes\n\nconst userRoute = Http.router.get(\n \"/users/:userId\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n getUser(req.params.userId)\n ).pipe(\n Effect.map(Http.response.json),\n // Manually catching errors inside the route logic\n Effect.catchTag(\"UserNotFoundError\", (e) =>\n Http.response.text(`User ${e.id} not found`, { status: 404 })\n ),\n Effect.catchTag(\"InvalidIdError\", (e) =>\n Http.response.text(`ID ${e.id} is not a valid format`, { status: 400 })\n )\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(userRoute));\n\n// No centralized error handling\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis approach is problematic because it pollutes the business logic of the route handler with details about HTTP status codes. It's also highly repetitive; if ten different routes could produce a `UserNotFoundError`, you would need to copy this `catchTag` logic into all ten of them, making the API difficult to maintain." - }, - { - "id": "handle-errors-with-catch", - "title": "Handle Errors with catchTag, catchTags, and catchAll", - "description": "Handle errors with catchTag, catchTags, and catchAll.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define domain types\ninterface User {\n readonly id: string;\n readonly name: string;\n}\n\n// Define specific error types\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly url: string;\n readonly code: number;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly id: string;\n}> {}\n\n// Define UserService\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n // Fetch user data\n fetchUser: (\n id: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user with id: ${id}`);\n\n if (id === \"invalid\") {\n const url = \"/api/users/\" + id;\n yield* Effect.logWarning(`Network error accessing: ${url}`);\n return yield* Effect.fail(new NetworkError({ url, code: 500 }));\n }\n\n if (id === \"missing\") {\n yield* Effect.logWarning(`User not found: ${id}`);\n return yield* Effect.fail(new NotFoundError({ id }));\n }\n\n const user = { id, name: \"John Doe\" };\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n return user;\n }),\n\n // Validate user data\n validateUser: (user: User): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`);\n\n if (user.name.length < 3) {\n yield* Effect.logWarning(\n `Validation failed: name too short for user ${user.id}`\n );\n return yield* Effect.fail(\n new ValidationError({ field: \"name\", message: \"Name too short\" })\n );\n }\n\n const message = `User ${user.name} is valid`;\n yield* Effect.logInfo(message);\n return message;\n }),\n }),\n}) {}\n\n// Compose operations with error handling using catchTags\nconst processUser = (\n userId: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const userService = yield* UserService;\n\n yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`);\n\n const result = yield* userService.fetchUser(userId).pipe(\n Effect.flatMap(userService.validateUser),\n // Handle different error types with specific recovery logic\n Effect.catchTags({\n NetworkError: (e) =>\n Effect.gen(function* () {\n const message = `Network error: ${e.code} for ${e.url}`;\n yield* Effect.logError(message);\n return message;\n }),\n NotFoundError: (e) =>\n Effect.gen(function* () {\n const message = `User ${e.id} not found`;\n yield* Effect.logWarning(message);\n return message;\n }),\n ValidationError: (e) =>\n Effect.gen(function* () {\n const message = `Invalid ${e.field}: ${e.message}`;\n yield* Effect.logWarning(message);\n return message;\n }),\n })\n );\n\n yield* Effect.logInfo(`Result: ${result}`);\n return result;\n });\n\n// Test with different scenarios\nconst runTests = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Starting User Processing Tests ===\");\n\n const testCases = [\"valid\", \"invalid\", \"missing\"];\n const results = yield* Effect.forEach(testCases, (id) => processUser(id));\n\n yield* Effect.logInfo(\"=== User Processing Tests Complete ===\");\n return results;\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(runTests, UserService.Default));\n```\n\n**Explanation:** \nUse `catchTag` to handle specific error types in a type-safe, composable way.", - "antiPattern": "Using `try/catch` blocks inside your Effect compositions. It breaks the\ndeclarative flow and bypasses Effect's powerful, type-safe error channels.", - "explanation": "Effect's structured error handling allows you to build resilient applications.\nBy using tagged errors and `catchTag`, you can handle different failure\nscenarios with different logic in a type-safe way.", - "content": "# Handle Errors with catchTag, catchTags, and catchAll\n\n## Guideline\n\nTo recover from failures, use the `catch*` family of functions.\n`Effect.catchTag` for specific tagged errors, `Effect.catchTags` for multiple,\nand `Effect.catchAll` for any error.\n\n## Rationale\n\nEffect's structured error handling allows you to build resilient applications.\nBy using tagged errors and `catchTag`, you can handle different failure\nscenarios with different logic in a type-safe way.\n\n## Good Example\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define domain types\ninterface User {\n readonly id: string;\n readonly name: string;\n}\n\n// Define specific error types\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly url: string;\n readonly code: number;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly id: string;\n}> {}\n\n// Define UserService\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n // Fetch user data\n fetchUser: (\n id: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user with id: ${id}`);\n\n if (id === \"invalid\") {\n const url = \"/api/users/\" + id;\n yield* Effect.logWarning(`Network error accessing: ${url}`);\n return yield* Effect.fail(new NetworkError({ url, code: 500 }));\n }\n\n if (id === \"missing\") {\n yield* Effect.logWarning(`User not found: ${id}`);\n return yield* Effect.fail(new NotFoundError({ id }));\n }\n\n const user = { id, name: \"John Doe\" };\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n return user;\n }),\n\n // Validate user data\n validateUser: (user: User): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`);\n\n if (user.name.length < 3) {\n yield* Effect.logWarning(\n `Validation failed: name too short for user ${user.id}`\n );\n return yield* Effect.fail(\n new ValidationError({ field: \"name\", message: \"Name too short\" })\n );\n }\n\n const message = `User ${user.name} is valid`;\n yield* Effect.logInfo(message);\n return message;\n }),\n }),\n}) {}\n\n// Compose operations with error handling using catchTags\nconst processUser = (\n userId: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const userService = yield* UserService;\n\n yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`);\n\n const result = yield* userService.fetchUser(userId).pipe(\n Effect.flatMap(userService.validateUser),\n // Handle different error types with specific recovery logic\n Effect.catchTags({\n NetworkError: (e) =>\n Effect.gen(function* () {\n const message = `Network error: ${e.code} for ${e.url}`;\n yield* Effect.logError(message);\n return message;\n }),\n NotFoundError: (e) =>\n Effect.gen(function* () {\n const message = `User ${e.id} not found`;\n yield* Effect.logWarning(message);\n return message;\n }),\n ValidationError: (e) =>\n Effect.gen(function* () {\n const message = `Invalid ${e.field}: ${e.message}`;\n yield* Effect.logWarning(message);\n return message;\n }),\n })\n );\n\n yield* Effect.logInfo(`Result: ${result}`);\n return result;\n });\n\n// Test with different scenarios\nconst runTests = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Starting User Processing Tests ===\");\n\n const testCases = [\"valid\", \"invalid\", \"missing\"];\n const results = yield* Effect.forEach(testCases, (id) => processUser(id));\n\n yield* Effect.logInfo(\"=== User Processing Tests Complete ===\");\n return results;\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(runTests, UserService.Default));\n```\n\n**Explanation:** \nUse `catchTag` to handle specific error types in a type-safe, composable way.\n\n## Anti-Pattern\n\nUsing `try/catch` blocks inside your Effect compositions. It breaks the\ndeclarative flow and bypasses Effect's powerful, type-safe error channels." - }, - { - "id": "handle-flaky-operations-with-retry-timeout", - "title": "Handle Flaky Operations with Retries and Timeouts", - "description": "Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "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.\n\n```typescript\nimport { Data, Duration, Effect, Schedule } from \"effect\";\n\n// Define domain types\ninterface ApiResponse {\n readonly data: string;\n}\n\n// Define error types\nclass ApiError extends Data.TaggedError(\"ApiError\")<{\n readonly message: string;\n readonly attempt: number;\n}> {}\n\nclass TimeoutError extends Data.TaggedError(\"TimeoutError\")<{\n readonly duration: string;\n readonly attempt: number;\n}> {}\n\n// Define API service\nclass ApiService extends Effect.Service()(\"ApiService\", {\n sync: () => ({\n // Flaky API call that might fail or be slow\n fetchData: (): Effect.Effect =>\n Effect.gen(function* () {\n const attempt = Math.floor(Math.random() * 5) + 1;\n yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`);\n\n if (Math.random() > 0.3) {\n yield* Effect.logWarning(`Attempt ${attempt}: API call failed`);\n return yield* Effect.fail(\n new ApiError({\n message: \"API Error\",\n attempt,\n })\n );\n }\n\n const delay = Math.random() * 3000;\n yield* Effect.logInfo(\n `Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms`\n );\n\n yield* Effect.sleep(Duration.millis(delay));\n\n const response = { data: \"some important data\" };\n yield* Effect.logInfo(\n `Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}`\n );\n return response;\n }),\n }),\n}) {}\n\n// Define retry policy: exponential backoff, up to 3 retries\nconst retryPolicy = Schedule.exponential(Duration.millis(100)).pipe(\n Schedule.compose(Schedule.recurs(3)),\n Schedule.tapInput((error: ApiError | TimeoutError) =>\n Effect.logWarning(\n `Retrying after error: ${error._tag} (Attempt ${error.attempt})`\n )\n )\n);\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const api = yield* ApiService;\n\n yield* Effect.logInfo(\"=== Starting API calls with retry and timeout ===\");\n\n // Make multiple test calls\n for (let i = 1; i <= 3; i++) {\n yield* Effect.logInfo(`\\n--- Test Call ${i} ---`);\n\n const result = yield* api.fetchData().pipe(\n Effect.timeout(Duration.seconds(2)),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new TimeoutError({ duration: \"2 seconds\", attempt: i }))\n ),\n Effect.retry(retryPolicy),\n Effect.catchTags({\n ApiError: (error) =>\n Effect.gen(function* () {\n yield* Effect.logError(\n `All retries failed: ${error.message} (Last attempt: ${error.attempt})`\n );\n return { data: \"fallback data due to API error\" } as ApiResponse;\n }),\n TimeoutError: (error) =>\n Effect.gen(function* () {\n yield* Effect.logError(\n `All retries timed out after ${error.duration} (Last attempt: ${error.attempt})`\n );\n return { data: \"fallback data due to timeout\" } as ApiResponse;\n }),\n })\n );\n\n yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`);\n }\n\n yield* Effect.logInfo(\"\\n=== API calls complete ===\");\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, ApiService.Default));\n```\n\n---", - "antiPattern": "Writing manual retry and timeout logic. This is verbose, complex, and easy to get wrong. It clutters your business logic with concerns that Effect can handle declaratively.\n\n```typescript\n// ❌ WRONG: Manual, complex, and error-prone logic.\nasync function manualRetryAndTimeout() {\n for (let i = 0; i < 3; i++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n const response = await fetch(\"...\", { signal: controller.signal });\n clearTimeout(timeoutId);\n\n return await response.json();\n } catch (error) {\n if (i === 2) throw error; // Last attempt, re-throw\n await new Promise((res) => setTimeout(res, 100 * 2 ** i)); // Manual backoff\n }\n }\n}\n```", - "explanation": "In distributed systems, failure is normal. APIs can fail intermittently, and network latency can spike. Hard-coding your application to try an operation only once makes it brittle.\n\n- **Retries:** The `Effect.retry` operator, combined with a `Schedule` policy, provides a powerful, declarative way to handle transient failures. Instead of writing complex `try/catch` loops, you can simply define a policy like \"retry 3 times, with an exponential backoff delay between attempts.\"\n\n- **Timeouts:** An operation might not fail, but instead hang indefinitely. `Effect.timeout` prevents this by racing your effect against a timer. If your effect doesn't complete within the specified duration, it is automatically interrupted, preventing your application from getting stuck.\n\nCombining these two patterns is a best practice for any interaction with an external service.\n\n---", - "content": "## Guideline\n\nTo build robust applications that can withstand unreliable external systems, apply two key operators to your effects:\n\n- **`Effect.retry(policy)`**: To automatically re-run a failing effect according to a schedule.\n- **`Effect.timeout(duration)`**: To interrupt an effect that takes too long to complete.\n\n---\n\n## Rationale\n\nIn distributed systems, failure is normal. APIs can fail intermittently, and network latency can spike. Hard-coding your application to try an operation only once makes it brittle.\n\n- **Retries:** The `Effect.retry` operator, combined with a `Schedule` policy, provides a powerful, declarative way to handle transient failures. Instead of writing complex `try/catch` loops, you can simply define a policy like \"retry 3 times, with an exponential backoff delay between attempts.\"\n\n- **Timeouts:** An operation might not fail, but instead hang indefinitely. `Effect.timeout` prevents this by racing your effect against a timer. If your effect doesn't complete within the specified duration, it is automatically interrupted, preventing your application from getting stuck.\n\nCombining these two patterns is a best practice for any interaction with an external service.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Data, Duration, Effect, Schedule } from \"effect\";\n\n// Define domain types\ninterface ApiResponse {\n readonly data: string;\n}\n\n// Define error types\nclass ApiError extends Data.TaggedError(\"ApiError\")<{\n readonly message: string;\n readonly attempt: number;\n}> {}\n\nclass TimeoutError extends Data.TaggedError(\"TimeoutError\")<{\n readonly duration: string;\n readonly attempt: number;\n}> {}\n\n// Define API service\nclass ApiService extends Effect.Service()(\"ApiService\", {\n sync: () => ({\n // Flaky API call that might fail or be slow\n fetchData: (): Effect.Effect =>\n Effect.gen(function* () {\n const attempt = Math.floor(Math.random() * 5) + 1;\n yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`);\n\n if (Math.random() > 0.3) {\n yield* Effect.logWarning(`Attempt ${attempt}: API call failed`);\n return yield* Effect.fail(\n new ApiError({\n message: \"API Error\",\n attempt,\n })\n );\n }\n\n const delay = Math.random() * 3000;\n yield* Effect.logInfo(\n `Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms`\n );\n\n yield* Effect.sleep(Duration.millis(delay));\n\n const response = { data: \"some important data\" };\n yield* Effect.logInfo(\n `Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}`\n );\n return response;\n }),\n }),\n}) {}\n\n// Define retry policy: exponential backoff, up to 3 retries\nconst retryPolicy = Schedule.exponential(Duration.millis(100)).pipe(\n Schedule.compose(Schedule.recurs(3)),\n Schedule.tapInput((error: ApiError | TimeoutError) =>\n Effect.logWarning(\n `Retrying after error: ${error._tag} (Attempt ${error.attempt})`\n )\n )\n);\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const api = yield* ApiService;\n\n yield* Effect.logInfo(\"=== Starting API calls with retry and timeout ===\");\n\n // Make multiple test calls\n for (let i = 1; i <= 3; i++) {\n yield* Effect.logInfo(`\\n--- Test Call ${i} ---`);\n\n const result = yield* api.fetchData().pipe(\n Effect.timeout(Duration.seconds(2)),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new TimeoutError({ duration: \"2 seconds\", attempt: i }))\n ),\n Effect.retry(retryPolicy),\n Effect.catchTags({\n ApiError: (error) =>\n Effect.gen(function* () {\n yield* Effect.logError(\n `All retries failed: ${error.message} (Last attempt: ${error.attempt})`\n );\n return { data: \"fallback data due to API error\" } as ApiResponse;\n }),\n TimeoutError: (error) =>\n Effect.gen(function* () {\n yield* Effect.logError(\n `All retries timed out after ${error.duration} (Last attempt: ${error.attempt})`\n );\n return { data: \"fallback data due to timeout\" } as ApiResponse;\n }),\n })\n );\n\n yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`);\n }\n\n yield* Effect.logInfo(\"\\n=== API calls complete ===\");\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, ApiService.Default));\n```\n\n---\n\n## Anti-Pattern\n\nWriting manual retry and timeout logic. This is verbose, complex, and easy to get wrong. It clutters your business logic with concerns that Effect can handle declaratively.\n\n```typescript\n// ❌ WRONG: Manual, complex, and error-prone logic.\nasync function manualRetryAndTimeout() {\n for (let i = 0; i < 3; i++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 2000);\n\n const response = await fetch(\"...\", { signal: controller.signal });\n clearTimeout(timeoutId);\n\n return await response.json();\n } catch (error) {\n if (i === 2) throw error; // Last attempt, re-throw\n await new Promise((res) => setTimeout(res, 100 * 2 ** i)); // Manual backoff\n }\n }\n}\n```" - }, - { - "id": "domain-modeling-option-basics", - "title": "Handle Missing Values with Option", - "description": "Use Option instead of null/undefined to make missing values explicit and type-safe.", - "skillLevel": "beginner", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Option, Effect } from \"effect\"\n\n// ============================================\n// 1. Creating Options\n// ============================================\n\n// Some - a value is present\nconst hasValue = Option.some(42)\n\n// None - no value\nconst noValue = Option.none()\n\n// From nullable - null/undefined becomes None\nconst fromNull = Option.fromNullable(null) // None\nconst fromValue = Option.fromNullable(\"hello\") // Some(\"hello\")\n\n// ============================================\n// 2. Checking and extracting values\n// ============================================\n\nconst maybeUser = Option.some({ name: \"Alice\", age: 30 })\n\n// Check if value exists\nif (Option.isSome(maybeUser)) {\n console.log(`User: ${maybeUser.value.name}`)\n}\n\n// Get with default\nconst name = Option.getOrElse(\n Option.map(maybeUser, u => u.name),\n () => \"Anonymous\"\n)\n\n// ============================================\n// 3. Transforming Options\n// ============================================\n\nconst maybeNumber = Option.some(5)\n\n// Map - transform the value if present\nconst doubled = Option.map(maybeNumber, n => n * 2) // Some(10)\n\n// FlatMap - chain operations that return Option\nconst safeDivide = (a: number, b: number): Option.Option =>\n b === 0 ? Option.none() : Option.some(a / b)\n\nconst result = Option.flatMap(maybeNumber, n => safeDivide(10, n)) // Some(2)\n\n// ============================================\n// 4. Domain modeling example\n// ============================================\n\ninterface User {\n readonly id: string\n readonly name: string\n readonly email: Option.Option // Email is optional\n readonly phone: Option.Option // Phone is optional\n}\n\nconst createUser = (name: string): User => ({\n id: crypto.randomUUID(),\n name,\n email: Option.none(),\n phone: Option.none(),\n})\n\nconst addEmail = (user: User, email: string): User => ({\n ...user,\n email: Option.some(email),\n})\n\nconst getContactInfo = (user: User): string => {\n const email = Option.getOrElse(user.email, () => \"no email\")\n const phone = Option.getOrElse(user.phone, () => \"no phone\")\n return `${user.name}: ${email}, ${phone}`\n}\n\n// ============================================\n// 5. Use in Effects\n// ============================================\n\nconst findUser = (id: string): Effect.Effect> =>\n Effect.succeed(\n id === \"123\"\n ? Option.some({ id, name: \"Alice\", email: Option.none(), phone: Option.none() })\n : Option.none()\n )\n\nconst program = Effect.gen(function* () {\n const maybeUser = yield* findUser(\"123\")\n\n if (Option.isSome(maybeUser)) {\n yield* Effect.log(`Found: ${maybeUser.value.name}`)\n } else {\n yield* Effect.log(\"User not found\")\n }\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "`null` and `undefined` cause bugs because:\n\n1. **Silent failures** - Accessing `.property` on null crashes at runtime\n2. **Unclear intent** - Is null \"not found\" or \"error\"?\n3. **Forgotten checks** - Easy to forget `if (x !== null)`\n\nOption fixes this by making absence explicit and type-checked.\n\n---", - "content": "## Guideline\n\nUse `Option` to represent values that might be absent. This makes \"might not exist\" explicit in your types, forcing you to handle both cases.\n\n---\n\n## Rationale\n\n`null` and `undefined` cause bugs because:\n\n1. **Silent failures** - Accessing `.property` on null crashes at runtime\n2. **Unclear intent** - Is null \"not found\" or \"error\"?\n3. **Forgotten checks** - Easy to forget `if (x !== null)`\n\nOption fixes this by making absence explicit and type-checked.\n\n---\n\n## Good Example\n\n```typescript\nimport { Option, Effect } from \"effect\"\n\n// ============================================\n// 1. Creating Options\n// ============================================\n\n// Some - a value is present\nconst hasValue = Option.some(42)\n\n// None - no value\nconst noValue = Option.none()\n\n// From nullable - null/undefined becomes None\nconst fromNull = Option.fromNullable(null) // None\nconst fromValue = Option.fromNullable(\"hello\") // Some(\"hello\")\n\n// ============================================\n// 2. Checking and extracting values\n// ============================================\n\nconst maybeUser = Option.some({ name: \"Alice\", age: 30 })\n\n// Check if value exists\nif (Option.isSome(maybeUser)) {\n console.log(`User: ${maybeUser.value.name}`)\n}\n\n// Get with default\nconst name = Option.getOrElse(\n Option.map(maybeUser, u => u.name),\n () => \"Anonymous\"\n)\n\n// ============================================\n// 3. Transforming Options\n// ============================================\n\nconst maybeNumber = Option.some(5)\n\n// Map - transform the value if present\nconst doubled = Option.map(maybeNumber, n => n * 2) // Some(10)\n\n// FlatMap - chain operations that return Option\nconst safeDivide = (a: number, b: number): Option.Option =>\n b === 0 ? Option.none() : Option.some(a / b)\n\nconst result = Option.flatMap(maybeNumber, n => safeDivide(10, n)) // Some(2)\n\n// ============================================\n// 4. Domain modeling example\n// ============================================\n\ninterface User {\n readonly id: string\n readonly name: string\n readonly email: Option.Option // Email is optional\n readonly phone: Option.Option // Phone is optional\n}\n\nconst createUser = (name: string): User => ({\n id: crypto.randomUUID(),\n name,\n email: Option.none(),\n phone: Option.none(),\n})\n\nconst addEmail = (user: User, email: string): User => ({\n ...user,\n email: Option.some(email),\n})\n\nconst getContactInfo = (user: User): string => {\n const email = Option.getOrElse(user.email, () => \"no email\")\n const phone = Option.getOrElse(user.phone, () => \"no phone\")\n return `${user.name}: ${email}, ${phone}`\n}\n\n// ============================================\n// 5. Use in Effects\n// ============================================\n\nconst findUser = (id: string): Effect.Effect> =>\n Effect.succeed(\n id === \"123\"\n ? Option.some({ id, name: \"Alice\", email: Option.none(), phone: Option.none() })\n : Option.none()\n )\n\nconst program = Effect.gen(function* () {\n const maybeUser = yield* findUser(\"123\")\n\n if (Option.isSome(maybeUser)) {\n yield* Effect.log(`Found: ${maybeUser.value.name}`)\n } else {\n yield* Effect.log(\"User not found\")\n }\n})\n\nEffect.runPromise(program)\n```\n\n## Option vs null\n\n| null/undefined | Option |\n|----------------|--------|\n| `null` | `Option.none()` |\n| `value` | `Option.some(value)` |\n| `x !== null` | `Option.isSome(x)` |\n| `x ?? default` | `Option.getOrElse(x, () => default)` |\n| `x?.property` | `Option.map(x, v => v.property)` |\n\n## When to Use Option\n\n- Optional fields in domain models\n- Database lookups that might return nothing\n- Configuration that has defaults\n- Any value that \"might not exist\"" - }, - { - "id": "http-rate-limit-handling", - "title": "Handle Rate Limiting Responses", - "description": "Detect 429 responses and automatically retry after the Retry-After period.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Schedule, Duration, Data, Ref } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Rate limit error type\n// ============================================\n\nclass RateLimitedError extends Data.TaggedError(\"RateLimitedError\")<{\n readonly retryAfter: number\n readonly limit: number | undefined\n readonly remaining: number | undefined\n readonly reset: number | undefined\n}> {}\n\n// ============================================\n// 2. Parse rate limit headers\n// ============================================\n\ninterface RateLimitInfo {\n readonly retryAfter: number\n readonly limit?: number\n readonly remaining?: number\n readonly reset?: number\n}\n\nconst parseRateLimitHeaders = (headers: Record): RateLimitInfo => {\n // Parse Retry-After (seconds or date)\n const retryAfterHeader = headers[\"retry-after\"]\n let retryAfter = 60 // Default 60 seconds\n\n if (retryAfterHeader) {\n const parsed = parseInt(retryAfterHeader, 10)\n if (!isNaN(parsed)) {\n retryAfter = parsed\n } else {\n // Try parsing as date\n const date = Date.parse(retryAfterHeader)\n if (!isNaN(date)) {\n retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000))\n }\n }\n }\n\n return {\n retryAfter,\n limit: headers[\"x-ratelimit-limit\"] ? parseInt(headers[\"x-ratelimit-limit\"], 10) : undefined,\n remaining: headers[\"x-ratelimit-remaining\"] ? parseInt(headers[\"x-ratelimit-remaining\"], 10) : undefined,\n reset: headers[\"x-ratelimit-reset\"] ? parseInt(headers[\"x-ratelimit-reset\"], 10) : undefined,\n }\n}\n\n// ============================================\n// 3. HTTP client with rate limit handling\n// ============================================\n\nconst makeRateLimitAwareClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n const response = yield* httpClient.get(url)\n\n if (response.status === 429) {\n const rateLimitInfo = parseRateLimitHeaders(response.headers)\n\n yield* Effect.log(\n `Rate limited. Retry after ${rateLimitInfo.retryAfter}s`\n )\n\n return yield* Effect.fail(new RateLimitedError({\n retryAfter: rateLimitInfo.retryAfter,\n limit: rateLimitInfo.limit,\n remaining: rateLimitInfo.remaining,\n reset: rateLimitInfo.reset,\n }))\n }\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }).pipe(\n Effect.retry({\n schedule: Schedule.recurWhile(\n (e) => e._tag === \"RateLimitedError\"\n ).pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.delayed((_, error) =>\n Duration.seconds(error.retryAfter + 1) // Add 1s buffer\n )\n ),\n while: (error) => error._tag === \"RateLimitedError\",\n })\n ),\n }\n})\n\n// ============================================\n// 4. Proactive rate limiting (client-side)\n// ============================================\n\ninterface RateLimiter {\n readonly acquire: () => Effect.Effect\n readonly release: () => Effect.Effect\n}\n\nconst makeClientRateLimiter = (requestsPerSecond: number) =>\n Effect.gen(function* () {\n const tokens = yield* Ref.make(requestsPerSecond)\n const interval = 1000 / requestsPerSecond\n\n // Refill tokens periodically\n yield* Effect.fork(\n Effect.forever(\n Effect.gen(function* () {\n yield* Effect.sleep(Duration.millis(interval))\n yield* Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond))\n })\n )\n )\n\n const limiter: RateLimiter = {\n acquire: () =>\n Effect.gen(function* () {\n let acquired = false\n while (!acquired) {\n const current = yield* Ref.get(tokens)\n if (current > 0) {\n yield* Ref.update(tokens, (n) => n - 1)\n acquired = true\n } else {\n yield* Effect.sleep(Duration.millis(interval))\n }\n }\n }),\n\n release: () => Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond)),\n }\n\n return limiter\n })\n\n// ============================================\n// 5. Combined client\n// ============================================\n\nconst makeRobustHttpClient = (requestsPerSecond: number) =>\n Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const rateLimiter = yield* makeClientRateLimiter(requestsPerSecond)\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n // Wait for rate limiter token\n yield* rateLimiter.acquire()\n\n const response = yield* httpClient.get(url)\n\n if (response.status === 429) {\n const info = parseRateLimitHeaders(response.headers)\n yield* Effect.log(`Server rate limit hit, waiting ${info.retryAfter}s`)\n yield* Effect.sleep(Duration.seconds(info.retryAfter))\n return yield* Effect.fail(new Error(\"Rate limited\"))\n }\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }).pipe(\n Effect.retry(\n Schedule.exponential(\"1 second\").pipe(\n Schedule.intersect(Schedule.recurs(3))\n )\n )\n ),\n }\n })\n\n// ============================================\n// 6. Batch requests to stay under limits\n// ============================================\n\nconst batchRequests = (\n urls: string[],\n requestsPerSecond: number\n) =>\n Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const results: T[] = []\n const interval = 1000 / requestsPerSecond\n\n for (const url of urls) {\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n results.push(data)\n\n // Wait between requests\n if (urls.indexOf(url) < urls.length - 1) {\n yield* Effect.sleep(Duration.millis(interval))\n }\n }\n\n return results\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeRateLimitAwareClient\n\n yield* Effect.log(\"Making rate-limited request...\")\n\n const data = yield* client.get(\"https://api.example.com/data\").pipe(\n Effect.catchTag(\"RateLimitedError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Gave up after rate limiting. Limit: ${error.limit}`)\n return { error: \"rate_limited\" }\n })\n )\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(data)}`)\n})\n```", - "antiPattern": "", - "explanation": "Rate limits protect APIs:\n\n1. **Fair usage** - Share resources among clients\n2. **Stability** - Prevent overload\n3. **Quotas** - Enforce billing tiers\n\nRespecting limits prevents bans and ensures reliable access.\n\n---", - "content": "## Guideline\n\nHandle HTTP 429 (Too Many Requests) responses by reading the `Retry-After` header and waiting before retrying.\n\n---\n\n## Rationale\n\nRate limits protect APIs:\n\n1. **Fair usage** - Share resources among clients\n2. **Stability** - Prevent overload\n3. **Quotas** - Enforce billing tiers\n\nRespecting limits prevents bans and ensures reliable access.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Schedule, Duration, Data, Ref } from \"effect\"\nimport { HttpClient, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Rate limit error type\n// ============================================\n\nclass RateLimitedError extends Data.TaggedError(\"RateLimitedError\")<{\n readonly retryAfter: number\n readonly limit: number | undefined\n readonly remaining: number | undefined\n readonly reset: number | undefined\n}> {}\n\n// ============================================\n// 2. Parse rate limit headers\n// ============================================\n\ninterface RateLimitInfo {\n readonly retryAfter: number\n readonly limit?: number\n readonly remaining?: number\n readonly reset?: number\n}\n\nconst parseRateLimitHeaders = (headers: Record): RateLimitInfo => {\n // Parse Retry-After (seconds or date)\n const retryAfterHeader = headers[\"retry-after\"]\n let retryAfter = 60 // Default 60 seconds\n\n if (retryAfterHeader) {\n const parsed = parseInt(retryAfterHeader, 10)\n if (!isNaN(parsed)) {\n retryAfter = parsed\n } else {\n // Try parsing as date\n const date = Date.parse(retryAfterHeader)\n if (!isNaN(date)) {\n retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000))\n }\n }\n }\n\n return {\n retryAfter,\n limit: headers[\"x-ratelimit-limit\"] ? parseInt(headers[\"x-ratelimit-limit\"], 10) : undefined,\n remaining: headers[\"x-ratelimit-remaining\"] ? parseInt(headers[\"x-ratelimit-remaining\"], 10) : undefined,\n reset: headers[\"x-ratelimit-reset\"] ? parseInt(headers[\"x-ratelimit-reset\"], 10) : undefined,\n }\n}\n\n// ============================================\n// 3. HTTP client with rate limit handling\n// ============================================\n\nconst makeRateLimitAwareClient = Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n const response = yield* httpClient.get(url)\n\n if (response.status === 429) {\n const rateLimitInfo = parseRateLimitHeaders(response.headers)\n\n yield* Effect.log(\n `Rate limited. Retry after ${rateLimitInfo.retryAfter}s`\n )\n\n return yield* Effect.fail(new RateLimitedError({\n retryAfter: rateLimitInfo.retryAfter,\n limit: rateLimitInfo.limit,\n remaining: rateLimitInfo.remaining,\n reset: rateLimitInfo.reset,\n }))\n }\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }).pipe(\n Effect.retry({\n schedule: Schedule.recurWhile(\n (e) => e._tag === \"RateLimitedError\"\n ).pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.delayed((_, error) =>\n Duration.seconds(error.retryAfter + 1) // Add 1s buffer\n )\n ),\n while: (error) => error._tag === \"RateLimitedError\",\n })\n ),\n }\n})\n\n// ============================================\n// 4. Proactive rate limiting (client-side)\n// ============================================\n\ninterface RateLimiter {\n readonly acquire: () => Effect.Effect\n readonly release: () => Effect.Effect\n}\n\nconst makeClientRateLimiter = (requestsPerSecond: number) =>\n Effect.gen(function* () {\n const tokens = yield* Ref.make(requestsPerSecond)\n const interval = 1000 / requestsPerSecond\n\n // Refill tokens periodically\n yield* Effect.fork(\n Effect.forever(\n Effect.gen(function* () {\n yield* Effect.sleep(Duration.millis(interval))\n yield* Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond))\n })\n )\n )\n\n const limiter: RateLimiter = {\n acquire: () =>\n Effect.gen(function* () {\n let acquired = false\n while (!acquired) {\n const current = yield* Ref.get(tokens)\n if (current > 0) {\n yield* Ref.update(tokens, (n) => n - 1)\n acquired = true\n } else {\n yield* Effect.sleep(Duration.millis(interval))\n }\n }\n }),\n\n release: () => Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond)),\n }\n\n return limiter\n })\n\n// ============================================\n// 5. Combined client\n// ============================================\n\nconst makeRobustHttpClient = (requestsPerSecond: number) =>\n Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const rateLimiter = yield* makeClientRateLimiter(requestsPerSecond)\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n // Wait for rate limiter token\n yield* rateLimiter.acquire()\n\n const response = yield* httpClient.get(url)\n\n if (response.status === 429) {\n const info = parseRateLimitHeaders(response.headers)\n yield* Effect.log(`Server rate limit hit, waiting ${info.retryAfter}s`)\n yield* Effect.sleep(Duration.seconds(info.retryAfter))\n return yield* Effect.fail(new Error(\"Rate limited\"))\n }\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }).pipe(\n Effect.retry(\n Schedule.exponential(\"1 second\").pipe(\n Schedule.intersect(Schedule.recurs(3))\n )\n )\n ),\n }\n })\n\n// ============================================\n// 6. Batch requests to stay under limits\n// ============================================\n\nconst batchRequests = (\n urls: string[],\n requestsPerSecond: number\n) =>\n Effect.gen(function* () {\n const httpClient = yield* HttpClient.HttpClient\n const results: T[] = []\n const interval = 1000 / requestsPerSecond\n\n for (const url of urls) {\n const response = yield* httpClient.get(url)\n const data = yield* HttpClientResponse.json(response) as Effect.Effect\n results.push(data)\n\n // Wait between requests\n if (urls.indexOf(url) < urls.length - 1) {\n yield* Effect.sleep(Duration.millis(interval))\n }\n }\n\n return results\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeRateLimitAwareClient\n\n yield* Effect.log(\"Making rate-limited request...\")\n\n const data = yield* client.get(\"https://api.example.com/data\").pipe(\n Effect.catchTag(\"RateLimitedError\", (error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Gave up after rate limiting. Limit: ${error.limit}`)\n return { error: \"rate_limited\" }\n })\n )\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(data)}`)\n})\n```\n\n## Rate Limit Headers\n\n| Header | Purpose |\n|--------|---------|\n| `Retry-After` | Seconds to wait |\n| `X-RateLimit-Limit` | Max requests allowed |\n| `X-RateLimit-Remaining` | Requests left |\n| `X-RateLimit-Reset` | Unix timestamp when limit resets |\n\n## Best Practices\n\n1. **Respect Retry-After** - Server tells you when to retry\n2. **Add buffer time** - Wait slightly longer than required\n3. **Client-side limiting** - Don't hit limits in the first place\n4. **Exponential backoff** - If still limited, wait longer\n5. **Monitor usage** - Track remaining quota" - }, - { - "id": "resource-timeouts", - "title": "Handle Resource Timeouts", - "description": "Always set timeouts on resource acquisition to prevent indefinite waits.", - "skillLevel": "intermediate", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Duration, Scope } from \"effect\"\n\n// ============================================\n// 1. Define a resource with slow acquisition\n// ============================================\n\ninterface Connection {\n readonly id: string\n readonly query: (sql: string) => Effect.Effect\n}\n\nconst acquireConnection = Effect.gen(function* () {\n yield* Effect.log(\"Attempting to connect...\")\n \n // Simulate slow connection\n yield* Effect.sleep(\"2 seconds\")\n \n const connection: Connection = {\n id: crypto.randomUUID(),\n query: (sql) => Effect.succeed({ rows: [] }),\n }\n \n yield* Effect.log(`Connected: ${connection.id}`)\n return connection\n})\n\nconst releaseConnection = (conn: Connection) =>\n Effect.log(`Released: ${conn.id}`)\n\n// ============================================\n// 2. Timeout on acquisition\n// ============================================\n\nconst acquireWithTimeout = acquireConnection.pipe(\n Effect.timeout(\"1 second\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(\"Connection timeout - database unreachable\"))\n )\n)\n\n// ============================================\n// 3. Timeout on usage\n// ============================================\n\nconst queryWithTimeout = (conn: Connection, sql: string) =>\n conn.query(sql).pipe(\n Effect.timeout(\"5 seconds\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(`Query timeout: ${sql}`))\n )\n )\n\n// ============================================\n// 4. Full resource lifecycle with timeouts\n// ============================================\n\nconst useConnectionWithTimeouts = Effect.acquireRelease(\n acquireWithTimeout,\n releaseConnection\n).pipe(\n Effect.flatMap((conn) =>\n Effect.gen(function* () {\n yield* Effect.log(\"Running queries...\")\n \n // Each query has its own timeout\n const result1 = yield* queryWithTimeout(conn, \"SELECT 1\")\n const result2 = yield* queryWithTimeout(conn, \"SELECT 2\")\n \n return [result1, result2]\n })\n ),\n Effect.scoped\n)\n\n// ============================================\n// 5. Timeout on entire operation\n// ============================================\n\nconst entireOperationWithTimeout = useConnectionWithTimeouts.pipe(\n Effect.timeout(\"10 seconds\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(\"Entire operation timed out\"))\n )\n)\n\n// ============================================\n// 6. Run with different scenarios\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Testing timeouts ===\")\n \n const result = yield* entireOperationWithTimeout.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed: ${error.message}`)\n return []\n })\n )\n )\n \n yield* Effect.log(`Result: ${JSON.stringify(result)}`)\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Resources can become unavailable:\n\n1. **Network partitions** - Can't reach database\n2. **Pool exhaustion** - All connections in use\n3. **Deadlocks** - Resources held indefinitely\n4. **Slow operations** - Query takes too long\n\nTimeouts provide a safety net.\n\n---", - "content": "## Guideline\n\nSet timeouts on resource acquisition and usage to ensure your application doesn't hang waiting for unavailable resources.\n\n---\n\n## Rationale\n\nResources can become unavailable:\n\n1. **Network partitions** - Can't reach database\n2. **Pool exhaustion** - All connections in use\n3. **Deadlocks** - Resources held indefinitely\n4. **Slow operations** - Query takes too long\n\nTimeouts provide a safety net.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Duration, Scope } from \"effect\"\n\n// ============================================\n// 1. Define a resource with slow acquisition\n// ============================================\n\ninterface Connection {\n readonly id: string\n readonly query: (sql: string) => Effect.Effect\n}\n\nconst acquireConnection = Effect.gen(function* () {\n yield* Effect.log(\"Attempting to connect...\")\n \n // Simulate slow connection\n yield* Effect.sleep(\"2 seconds\")\n \n const connection: Connection = {\n id: crypto.randomUUID(),\n query: (sql) => Effect.succeed({ rows: [] }),\n }\n \n yield* Effect.log(`Connected: ${connection.id}`)\n return connection\n})\n\nconst releaseConnection = (conn: Connection) =>\n Effect.log(`Released: ${conn.id}`)\n\n// ============================================\n// 2. Timeout on acquisition\n// ============================================\n\nconst acquireWithTimeout = acquireConnection.pipe(\n Effect.timeout(\"1 second\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(\"Connection timeout - database unreachable\"))\n )\n)\n\n// ============================================\n// 3. Timeout on usage\n// ============================================\n\nconst queryWithTimeout = (conn: Connection, sql: string) =>\n conn.query(sql).pipe(\n Effect.timeout(\"5 seconds\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(`Query timeout: ${sql}`))\n )\n )\n\n// ============================================\n// 4. Full resource lifecycle with timeouts\n// ============================================\n\nconst useConnectionWithTimeouts = Effect.acquireRelease(\n acquireWithTimeout,\n releaseConnection\n).pipe(\n Effect.flatMap((conn) =>\n Effect.gen(function* () {\n yield* Effect.log(\"Running queries...\")\n \n // Each query has its own timeout\n const result1 = yield* queryWithTimeout(conn, \"SELECT 1\")\n const result2 = yield* queryWithTimeout(conn, \"SELECT 2\")\n \n return [result1, result2]\n })\n ),\n Effect.scoped\n)\n\n// ============================================\n// 5. Timeout on entire operation\n// ============================================\n\nconst entireOperationWithTimeout = useConnectionWithTimeouts.pipe(\n Effect.timeout(\"10 seconds\"),\n Effect.catchTag(\"TimeoutException\", () =>\n Effect.fail(new Error(\"Entire operation timed out\"))\n )\n)\n\n// ============================================\n// 6. Run with different scenarios\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Testing timeouts ===\")\n \n const result = yield* entireOperationWithTimeout.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed: ${error.message}`)\n return []\n })\n )\n )\n \n yield* Effect.log(`Result: ${JSON.stringify(result)}`)\n})\n\nEffect.runPromise(program)\n```\n\n## Timeout Strategies\n\n| Level | Purpose | Typical Value |\n|-------|---------|---------------|\n| Acquisition | Connect to resource | 1-5 seconds |\n| Per-operation | Single query/call | 5-30 seconds |\n| Total operation | Entire workflow | 30-120 seconds |\n\n## Timeout with Interrupt\n\n```typescript\n// Timeout that interrupts the fiber\nconst withInterrupt = slowOperation.pipe(\n Effect.timeoutFail({\n duration: \"5 seconds\",\n onTimeout: () => new TimeoutError(\"Operation took too long\"),\n })\n)\n\n// Timeout that returns Option\nconst withOption = slowOperation.pipe(\n Effect.timeout(\"5 seconds\")\n // Returns Option - None if timed out\n)\n```\n\n## Best Practices\n\n1. **Layer timeouts** - Different timeouts for different operations\n2. **Log timeouts** - Know when they happen\n3. **Provide fallbacks** - What to do when timeout occurs\n4. **Don't set too short** - Allow for normal variance\n5. **Monitor timeout rates** - High rate indicates issues" - }, - { - "id": "handle-unexpected-errors-with-cause", - "title": "Handle Unexpected Errors by Inspecting the Cause", - "description": "Handle unexpected errors by inspecting the cause.", - "skillLevel": "advanced", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Cause, Effect, Data, Schedule, Duration } from \"effect\";\n\n// Define domain types\ninterface DatabaseConfig {\n readonly url: string;\n}\n\ninterface DatabaseConnection {\n readonly success: true;\n}\n\ninterface UserData {\n readonly id: string;\n readonly name: string;\n}\n\n// Define error types\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n readonly operation: string;\n readonly details: string;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\n// Define database service\nclass DatabaseService extends Effect.Service()(\n \"DatabaseService\",\n {\n sync: () => ({\n // Connect to database with proper error handling\n connect: (\n config: DatabaseConfig\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Connecting to database: ${config.url}`);\n\n if (!config.url) {\n const error = new DatabaseError({\n operation: \"connect\",\n details: \"Missing URL\",\n });\n yield* Effect.logError(`Database error: ${JSON.stringify(error)}`);\n return yield* Effect.fail(error);\n }\n\n // Simulate unexpected errors\n if (config.url === \"invalid\") {\n yield* Effect.logError(\"Invalid connection string\");\n return yield* Effect.sync(() => {\n throw new Error(\"Failed to parse connection string\");\n });\n }\n\n if (config.url === \"timeout\") {\n yield* Effect.logError(\"Connection timeout\");\n return yield* Effect.sync(() => {\n throw new Error(\"Connection timed out\");\n });\n }\n\n yield* Effect.logInfo(\"Database connection successful\");\n return { success: true };\n }),\n }),\n }\n) {}\n\n// Define user service\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n // Parse user data with validation\n parseUser: (input: unknown): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Parsing user data: ${JSON.stringify(input)}`);\n\n try {\n if (typeof input !== \"object\" || !input) {\n const error = new ValidationError({\n field: \"input\",\n message: \"Invalid input type\",\n });\n yield* Effect.logWarning(\n `Validation error: ${JSON.stringify(error)}`\n );\n throw error;\n }\n\n const data = input as Record;\n\n if (typeof data.id !== \"string\" || typeof data.name !== \"string\") {\n const error = new ValidationError({\n field: \"input\",\n message: \"Missing required fields\",\n });\n yield* Effect.logWarning(\n `Validation error: ${JSON.stringify(error)}`\n );\n throw error;\n }\n\n const user = { id: data.id, name: data.name };\n yield* Effect.logInfo(\n `Successfully parsed user: ${JSON.stringify(user)}`\n );\n return user;\n } catch (e) {\n if (e instanceof ValidationError) {\n return yield* Effect.fail(e);\n }\n yield* Effect.logError(\n `Unexpected error: ${e instanceof Error ? e.message : String(e)}`\n );\n throw e;\n }\n }),\n }),\n}) {}\n\n// Define test service\nclass TestService extends Effect.Service()(\"TestService\", {\n sync: () => {\n // Create instance methods\n const printCause = (\n prefix: string,\n cause: Cause.Cause\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`\\n=== ${prefix} ===`);\n\n if (Cause.isDie(cause)) {\n const defect = Cause.failureOption(cause);\n if (defect._tag === \"Some\") {\n const error = defect.value as Error;\n yield* Effect.logError(\"Defect (unexpected error)\");\n yield* Effect.logError(`Message: ${error.message}`);\n yield* Effect.logError(\n `Stack: ${error.stack?.split(\"\\n\")[1]?.trim() ?? \"N/A\"}`\n );\n }\n } else if (Cause.isFailure(cause)) {\n const error = Cause.failureOption(cause);\n yield* Effect.logWarning(\"Expected failure\");\n yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`);\n }\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n });\n\n const runScenario = (\n name: string,\n program: Effect.Effect\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`\\n=== Testing: ${name} ===`);\n\n type TestError = {\n readonly _tag: \"error\";\n readonly cause: Cause.Cause;\n };\n\n const result = yield* Effect.catchAllCause(program, (cause) =>\n Effect.succeed({ _tag: \"error\" as const, cause } as TestError)\n );\n\n if (\"cause\" in result) {\n yield* printCause(\"Error details\", result.cause);\n } else {\n yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);\n }\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n });\n\n // Return bound methods\n return {\n printCause,\n runScenario,\n };\n },\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const db = yield* DatabaseService;\n const users = yield* UserService;\n const test = yield* TestService;\n\n yield* Effect.logInfo(\"=== Starting Error Handling Tests ===\");\n\n // Test expected database errors\n yield* test.runScenario(\n \"Expected database error\",\n Effect.gen(function* () {\n const result = yield* Effect.retry(\n db.connect({ url: \"\" }),\n Schedule.exponential(100)\n ).pipe(\n Effect.timeout(Duration.seconds(5)),\n Effect.catchAll(() => Effect.fail(\"Connection timeout\"))\n );\n return result;\n })\n );\n\n // Test unexpected connection errors\n yield* test.runScenario(\n \"Unexpected connection error\",\n Effect.gen(function* () {\n const result = yield* Effect.retry(\n db.connect({ url: \"invalid\" }),\n Schedule.recurs(3)\n ).pipe(\n Effect.catchAllCause((cause) =>\n Effect.gen(function* () {\n yield* Effect.logError(\"Failed after 3 retries\");\n yield* Effect.logError(Cause.pretty(cause));\n return yield* Effect.fail(\"Max retries exceeded\");\n })\n )\n );\n return result;\n })\n );\n\n // Test user validation with recovery\n yield* test.runScenario(\n \"Valid user data\",\n Effect.gen(function* () {\n const result = yield* users\n .parseUser({ id: \"1\", name: \"John\" })\n .pipe(\n Effect.orElse(() =>\n Effect.succeed({ id: \"default\", name: \"Default User\" })\n )\n );\n return result;\n })\n );\n\n // Test concurrent error handling with timeout\n yield* test.runScenario(\n \"Concurrent operations\",\n Effect.gen(function* () {\n const results = yield* Effect.all(\n [\n db.connect({ url: \"\" }).pipe(\n Effect.timeout(Duration.seconds(1)),\n Effect.catchAll(() => Effect.succeed({ success: true }))\n ),\n users.parseUser({ id: \"invalid\" }).pipe(\n Effect.timeout(Duration.seconds(1)),\n Effect.catchAll(() =>\n Effect.succeed({ id: \"timeout\", name: \"Timeout\" })\n )\n ),\n ],\n { concurrency: 2 }\n );\n return results;\n })\n );\n\n yield* Effect.logInfo(\"\\n=== Error Handling Tests Complete ===\");\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n});\n\n// Run the program with all services\nEffect.runPromise(\n Effect.provide(\n Effect.provide(\n Effect.provide(program, TestService.Default),\n DatabaseService.Default\n ),\n UserService.Default\n )\n);\n```\n\n**Explanation:** \nBy inspecting the `Cause`, you can distinguish between expected and unexpected\nfailures, logging or escalating as appropriate.", - "antiPattern": "Using a simple `Effect.catchAll` can dangerously conflate expected errors and\nunexpected defects, masking critical bugs as recoverable errors.", - "explanation": "The `Cause` object explains _why_ an effect failed. A `Fail` is an expected\nerror (e.g., `ValidationError`). A `Die` is an unexpected defect (e.g., a\nthrown exception). They should be handled differently.", - "content": "# Handle Unexpected Errors by Inspecting the Cause\n\n## Guideline\n\nTo build truly resilient applications, differentiate between known business\nerrors (`Fail`) and unknown defects (`Die`). Use `Effect.catchAllCause` to\ninspect the full `Cause` of a failure.\n\n## Rationale\n\nThe `Cause` object explains _why_ an effect failed. A `Fail` is an expected\nerror (e.g., `ValidationError`). A `Die` is an unexpected defect (e.g., a\nthrown exception). They should be handled differently.\n\n## Good Example\n\n```typescript\nimport { Cause, Effect, Data, Schedule, Duration } from \"effect\";\n\n// Define domain types\ninterface DatabaseConfig {\n readonly url: string;\n}\n\ninterface DatabaseConnection {\n readonly success: true;\n}\n\ninterface UserData {\n readonly id: string;\n readonly name: string;\n}\n\n// Define error types\nclass DatabaseError extends Data.TaggedError(\"DatabaseError\")<{\n readonly operation: string;\n readonly details: string;\n}> {}\n\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n readonly field: string;\n readonly message: string;\n}> {}\n\n// Define database service\nclass DatabaseService extends Effect.Service()(\n \"DatabaseService\",\n {\n sync: () => ({\n // Connect to database with proper error handling\n connect: (\n config: DatabaseConfig\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Connecting to database: ${config.url}`);\n\n if (!config.url) {\n const error = new DatabaseError({\n operation: \"connect\",\n details: \"Missing URL\",\n });\n yield* Effect.logError(`Database error: ${JSON.stringify(error)}`);\n return yield* Effect.fail(error);\n }\n\n // Simulate unexpected errors\n if (config.url === \"invalid\") {\n yield* Effect.logError(\"Invalid connection string\");\n return yield* Effect.sync(() => {\n throw new Error(\"Failed to parse connection string\");\n });\n }\n\n if (config.url === \"timeout\") {\n yield* Effect.logError(\"Connection timeout\");\n return yield* Effect.sync(() => {\n throw new Error(\"Connection timed out\");\n });\n }\n\n yield* Effect.logInfo(\"Database connection successful\");\n return { success: true };\n }),\n }),\n }\n) {}\n\n// Define user service\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n // Parse user data with validation\n parseUser: (input: unknown): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Parsing user data: ${JSON.stringify(input)}`);\n\n try {\n if (typeof input !== \"object\" || !input) {\n const error = new ValidationError({\n field: \"input\",\n message: \"Invalid input type\",\n });\n yield* Effect.logWarning(\n `Validation error: ${JSON.stringify(error)}`\n );\n throw error;\n }\n\n const data = input as Record;\n\n if (typeof data.id !== \"string\" || typeof data.name !== \"string\") {\n const error = new ValidationError({\n field: \"input\",\n message: \"Missing required fields\",\n });\n yield* Effect.logWarning(\n `Validation error: ${JSON.stringify(error)}`\n );\n throw error;\n }\n\n const user = { id: data.id, name: data.name };\n yield* Effect.logInfo(\n `Successfully parsed user: ${JSON.stringify(user)}`\n );\n return user;\n } catch (e) {\n if (e instanceof ValidationError) {\n return yield* Effect.fail(e);\n }\n yield* Effect.logError(\n `Unexpected error: ${e instanceof Error ? e.message : String(e)}`\n );\n throw e;\n }\n }),\n }),\n}) {}\n\n// Define test service\nclass TestService extends Effect.Service()(\"TestService\", {\n sync: () => {\n // Create instance methods\n const printCause = (\n prefix: string,\n cause: Cause.Cause\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`\\n=== ${prefix} ===`);\n\n if (Cause.isDie(cause)) {\n const defect = Cause.failureOption(cause);\n if (defect._tag === \"Some\") {\n const error = defect.value as Error;\n yield* Effect.logError(\"Defect (unexpected error)\");\n yield* Effect.logError(`Message: ${error.message}`);\n yield* Effect.logError(\n `Stack: ${error.stack?.split(\"\\n\")[1]?.trim() ?? \"N/A\"}`\n );\n }\n } else if (Cause.isFailure(cause)) {\n const error = Cause.failureOption(cause);\n yield* Effect.logWarning(\"Expected failure\");\n yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`);\n }\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n });\n\n const runScenario = (\n name: string,\n program: Effect.Effect\n ): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`\\n=== Testing: ${name} ===`);\n\n type TestError = {\n readonly _tag: \"error\";\n readonly cause: Cause.Cause;\n };\n\n const result = yield* Effect.catchAllCause(program, (cause) =>\n Effect.succeed({ _tag: \"error\" as const, cause } as TestError)\n );\n\n if (\"cause\" in result) {\n yield* printCause(\"Error details\", result.cause);\n } else {\n yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);\n }\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n });\n\n // Return bound methods\n return {\n printCause,\n runScenario,\n };\n },\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const db = yield* DatabaseService;\n const users = yield* UserService;\n const test = yield* TestService;\n\n yield* Effect.logInfo(\"=== Starting Error Handling Tests ===\");\n\n // Test expected database errors\n yield* test.runScenario(\n \"Expected database error\",\n Effect.gen(function* () {\n const result = yield* Effect.retry(\n db.connect({ url: \"\" }),\n Schedule.exponential(100)\n ).pipe(\n Effect.timeout(Duration.seconds(5)),\n Effect.catchAll(() => Effect.fail(\"Connection timeout\"))\n );\n return result;\n })\n );\n\n // Test unexpected connection errors\n yield* test.runScenario(\n \"Unexpected connection error\",\n Effect.gen(function* () {\n const result = yield* Effect.retry(\n db.connect({ url: \"invalid\" }),\n Schedule.recurs(3)\n ).pipe(\n Effect.catchAllCause((cause) =>\n Effect.gen(function* () {\n yield* Effect.logError(\"Failed after 3 retries\");\n yield* Effect.logError(Cause.pretty(cause));\n return yield* Effect.fail(\"Max retries exceeded\");\n })\n )\n );\n return result;\n })\n );\n\n // Test user validation with recovery\n yield* test.runScenario(\n \"Valid user data\",\n Effect.gen(function* () {\n const result = yield* users\n .parseUser({ id: \"1\", name: \"John\" })\n .pipe(\n Effect.orElse(() =>\n Effect.succeed({ id: \"default\", name: \"Default User\" })\n )\n );\n return result;\n })\n );\n\n // Test concurrent error handling with timeout\n yield* test.runScenario(\n \"Concurrent operations\",\n Effect.gen(function* () {\n const results = yield* Effect.all(\n [\n db.connect({ url: \"\" }).pipe(\n Effect.timeout(Duration.seconds(1)),\n Effect.catchAll(() => Effect.succeed({ success: true }))\n ),\n users.parseUser({ id: \"invalid\" }).pipe(\n Effect.timeout(Duration.seconds(1)),\n Effect.catchAll(() =>\n Effect.succeed({ id: \"timeout\", name: \"Timeout\" })\n )\n ),\n ],\n { concurrency: 2 }\n );\n return results;\n })\n );\n\n yield* Effect.logInfo(\"\\n=== Error Handling Tests Complete ===\");\n\n // Don't return an Effect inside Effect.gen, just return the value directly\n return void 0;\n});\n\n// Run the program with all services\nEffect.runPromise(\n Effect.provide(\n Effect.provide(\n Effect.provide(program, TestService.Default),\n DatabaseService.Default\n ),\n UserService.Default\n )\n);\n```\n\n**Explanation:** \nBy inspecting the `Cause`, you can distinguish between expected and unexpected\nfailures, logging or escalating as appropriate.\n\n## Anti-Pattern\n\nUsing a simple `Effect.catchAll` can dangerously conflate expected errors and\nunexpected defects, masking critical bugs as recoverable errors." - }, - { - "id": "data-cause", - "title": "Handle Unexpected Errors by Inspecting the Cause", - "description": "Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions.", - "skillLevel": "advanced", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Cause, Effect } from \"effect\";\n\n// An Effect that may fail with an error or defect\nconst program = Effect.try({\n try: () => {\n throw new Error(\"Unexpected failure!\");\n },\n catch: (err) => err,\n});\n\n// Catch all causes and inspect them\nconst handled = program.pipe(\n Effect.catchAllCause((cause) =>\n Effect.sync(() => {\n if (Cause.isDie(cause)) {\n console.error(\"Defect (die):\", Cause.pretty(cause));\n } else if (Cause.isFailure(cause)) {\n console.error(\"Expected error:\", Cause.pretty(cause));\n } else if (Cause.isInterrupted(cause)) {\n console.error(\"Interrupted:\", Cause.pretty(cause));\n }\n // Handle or rethrow as needed\n })\n )\n);\n```\n\n**Explanation:**\n\n- `Cause` distinguishes between expected errors (`fail`), defects (`die`), and interruptions.\n- Use `Cause.pretty` for human-readable error traces.\n- Enables advanced error handling and debugging.", - "antiPattern": "Catching only expected errors and ignoring defects or interruptions, which can lead to silent failures, missed bugs, and harder debugging.", - "explanation": "Traditional error handling often loses information about _why_ a failure occurred. \n`Cause` preserves the full error context, enabling advanced debugging, error reporting, and robust recovery strategies.", - "content": "# Handle Unexpected Errors by Inspecting the `Cause`\n\n## Guideline\n\nUse the `Cause` data type to get rich, structured information about errors and failures in your Effects. \n`Cause` captures not just expected errors, but also defects (unhandled exceptions), interruptions, and error traces.\n\n## Rationale\n\nTraditional error handling often loses information about _why_ a failure occurred. \n`Cause` preserves the full error context, enabling advanced debugging, error reporting, and robust recovery strategies.\n\n## Good Example\n\n```typescript\nimport { Cause, Effect } from \"effect\";\n\n// An Effect that may fail with an error or defect\nconst program = Effect.try({\n try: () => {\n throw new Error(\"Unexpected failure!\");\n },\n catch: (err) => err,\n});\n\n// Catch all causes and inspect them\nconst handled = program.pipe(\n Effect.catchAllCause((cause) =>\n Effect.sync(() => {\n if (Cause.isDie(cause)) {\n console.error(\"Defect (die):\", Cause.pretty(cause));\n } else if (Cause.isFailure(cause)) {\n console.error(\"Expected error:\", Cause.pretty(cause));\n } else if (Cause.isInterrupted(cause)) {\n console.error(\"Interrupted:\", Cause.pretty(cause));\n }\n // Handle or rethrow as needed\n })\n )\n);\n```\n\n**Explanation:**\n\n- `Cause` distinguishes between expected errors (`fail`), defects (`die`), and interruptions.\n- Use `Cause.pretty` for human-readable error traces.\n- Enables advanced error handling and debugging.\n\n## Anti-Pattern\n\nCatching only expected errors and ignoring defects or interruptions, which can lead to silent failures, missed bugs, and harder debugging." - }, - { - "id": "getting-started-handle-errors", - "title": "Handle Your First Error with Effect.fail and catchAll", - "description": "Handle errors with Effect.fail and catchAll.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "```typescript\nimport { Effect, pipe } from \"effect\";\n\nclass UserNotFound {\n readonly _tag = \"UserNotFound\";\n constructor(readonly id: string) {}\n}\n\nconst findUser = (id: string) =>\n id === \"123\"\n ? Effect.succeed({ id, name: \"Alice\" })\n : Effect.fail(new UserNotFound(id));\n\nconst program = pipe(\n findUser(\"456\"),\n Effect.catchTag(\"UserNotFound\", (e) =>\n Effect.succeed({ id: e.id, name: \"Guest\" })\n ),\n Effect.map((user) => `Hello, ${user.name}!`)\n);\n\nconst result = Effect.runSync(program);\nconsole.log(result); // \"Hello, Guest!\"\n```", - "antiPattern": "", - "explanation": "Real programs fail. Effect makes failures explicit in the type system so you\ncan't forget to handle them. Unlike try/catch, Effect errors are tracked in\ntypes.", - "content": "# Handle Your First Error with Effect.fail and catchAll\n\n## Guideline\n\nUse `Effect.fail` to create an Effect that fails with an error, and\n`Effect.catchAll` to recover from that failure.\n\n## Rationale\n\nReal programs fail. Effect makes failures explicit in the type system so you\ncan't forget to handle them. Unlike try/catch, Effect errors are tracked in\ntypes.\n\n## Creating a Failing Effect\n\n```typescript\nimport { Effect } from \"effect\";\n\n// An Effect that always fails\nconst alwaysFails = Effect.fail(\"Something went wrong\");\n\n// An Effect that might fail based on a condition\nconst divide = (a: number, b: number) =>\n b === 0\n ? Effect.fail(\"Cannot divide by zero\")\n : Effect.succeed(a / b);\n```\n\n## Recovering from Errors\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\nconst divide = (a: number, b: number) =>\n b === 0\n ? Effect.fail(\"Division by zero\")\n : Effect.succeed(a / b);\n\n// Without error handling - this would fail\nconst unsafeResult = divide(10, 0);\n\n// With error handling - recover with a default value\nconst safeResult = pipe(\n divide(10, 0),\n Effect.catchAll((error) => {\n console.log(`Caught error: ${error}`);\n return Effect.succeed(0); // Return 0 as fallback\n })\n);\n\nEffect.runSync(safeResult); // 0\n```\n\n## Using Typed Errors\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\n// Define specific error types\nclass DivisionByZero {\n readonly _tag = \"DivisionByZero\";\n}\n\nclass NegativeNumber {\n readonly _tag = \"NegativeNumber\";\n constructor(readonly value: number) {}\n}\n\n// Function with typed errors\nconst safeDivide = (a: number, b: number): Effect.Effect => {\n if (b === 0) return Effect.fail(new DivisionByZero());\n if (a < 0) return Effect.fail(new NegativeNumber(a));\n return Effect.succeed(a / b);\n};\n\n// Handle all errors\nconst result = pipe(\n safeDivide(-10, 2),\n Effect.catchAll((error) => {\n switch (error._tag) {\n case \"DivisionByZero\":\n return Effect.succeed(0);\n case \"NegativeNumber\":\n return Effect.succeed(Math.abs(error.value));\n }\n })\n);\n```\n\n## Quick Reference\n\n| Function | Purpose |\n|----------|---------|\n| `Effect.fail(error)` | Create an Effect that fails |\n| `Effect.catchAll(handler)` | Catch all errors and recover |\n| `Effect.catchTag(tag, handler)` | Catch specific error by tag |\n| `Effect.orElse(fallback)` | Try a fallback Effect on error |\n| `Effect.orElseSucceed(value)` | Return a fallback value on error |\n\n## Good Example: Real-World Pattern\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\nclass UserNotFound {\n readonly _tag = \"UserNotFound\";\n constructor(readonly id: string) {}\n}\n\nconst findUser = (id: string) =>\n id === \"123\"\n ? Effect.succeed({ id, name: \"Alice\" })\n : Effect.fail(new UserNotFound(id));\n\nconst program = pipe(\n findUser(\"456\"),\n Effect.catchTag(\"UserNotFound\", (e) =>\n Effect.succeed({ id: e.id, name: \"Guest\" })\n ),\n Effect.map((user) => `Hello, ${user.name}!`)\n);\n\nconst result = Effect.runSync(program);\nconsole.log(result); // \"Hello, Guest!\"\n```\n\n## Key Points\n\n1. **Effect.fail** creates a failing Effect - the error is part of the type\n2. **Effect.catchAll** handles any error and must return an Effect\n3. **Tagged errors** (with `_tag`) let you handle specific errors with `catchTag`\n4. **Errors are values** - not thrown exceptions - making them composable\n\n## What's Next?\n\n- Learn about accumulating multiple errors\n- Learn about retrying failed operations\n- Learn about typed error hierarchies" - }, - { - "id": "combinator-error-handling", - "title": "Handling Errors with catchAll, orElse, and match", - "description": "Use error handling combinators to recover from failures, provide fallback values, or transform errors in a composable way.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Recover from any error\nconst effect = Effect.fail(\"fail!\").pipe(\n Effect.catchAll((err) => Effect.succeed(`Recovered from: ${err}`))\n); // Effect\n\n// Option: Provide a fallback if value is None\nconst option = Option.none().pipe(Option.orElse(() => Option.some(\"default\"))); // Option\n\n// Either: Provide a fallback if value is Left\nconst either = Either.left(\"error\").pipe(\n Either.orElse(() => Either.right(\"fallback\"))\n); // Either\n\n// Effect: Pattern match on success or failure\nconst matchEffect = Effect.fail(\"fail!\").pipe(\n Effect.match({\n onFailure: (err) => `Error: ${err}`,\n onSuccess: (value) => `Success: ${value}`,\n })\n); // Effect\n```\n\n**Explanation:** \nThese combinators let you handle errors, provide defaults, or transform error values in a way that is composable and type-safe. \nYou can recover from errors, provide alternative computations, or pattern match on success/failure.", - "antiPattern": "Using try/catch, null checks, or imperative error handling outside the combinator world. \nThis breaks composability, loses type safety, and makes error propagation unpredictable.", - "explanation": "Error handling is a first-class concern in functional programming. \nBy using combinators, you keep error recovery logic close to where errors may occur, and avoid scattering try/catch or null checks throughout your code.", - "content": "# Handling Errors with `catchAll`, `orElse`, and `match`\n\n## Guideline\n\nUse combinators like `catchAll`, `orElse`, and `match` to handle errors declaratively. \nThese allow you to recover from failures, provide fallback values, or transform errors, all while preserving composability and type safety.\n\n## Rationale\n\nError handling is a first-class concern in functional programming. \nBy using combinators, you keep error recovery logic close to where errors may occur, and avoid scattering try/catch or null checks throughout your code.\n\n## Good Example\n\n```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Recover from any error\nconst effect = Effect.fail(\"fail!\").pipe(\n Effect.catchAll((err) => Effect.succeed(`Recovered from: ${err}`))\n); // Effect\n\n// Option: Provide a fallback if value is None\nconst option = Option.none().pipe(Option.orElse(() => Option.some(\"default\"))); // Option\n\n// Either: Provide a fallback if value is Left\nconst either = Either.left(\"error\").pipe(\n Either.orElse(() => Either.right(\"fallback\"))\n); // Either\n\n// Effect: Pattern match on success or failure\nconst matchEffect = Effect.fail(\"fail!\").pipe(\n Effect.match({\n onFailure: (err) => `Error: ${err}`,\n onSuccess: (value) => `Success: ${value}`,\n })\n); // Effect\n```\n\n**Explanation:** \nThese combinators let you handle errors, provide defaults, or transform error values in a way that is composable and type-safe. \nYou can recover from errors, provide alternative computations, or pattern match on success/failure.\n\n## Anti-Pattern\n\nUsing try/catch, null checks, or imperative error handling outside the combinator world. \nThis breaks composability, loses type safety, and makes error propagation unpredictable." - }, - { - "id": "pattern-catchtag", - "title": "Handling Specific Errors with catchTag and catchTags", - "description": "Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define tagged error types\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{}> {}\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n message: string;\n}> {}\n\ntype MyError = NotFoundError | ValidationError;\n\n// Effect: Handle only ValidationError, let others propagate\nconst effect = Effect.fail(\n new ValidationError({ message: \"Invalid input\" }) as MyError\n).pipe(\n Effect.catchTag(\"ValidationError\", (err) =>\n Effect.succeed(`Recovered from validation error: ${err.message}`)\n )\n); // Effect\n\n// Effect: Handle multiple error tags\nconst effect2 = Effect.fail(new NotFoundError() as MyError).pipe(\n Effect.catchTags({\n NotFoundError: () => Effect.succeed(\"Handled not found!\"),\n ValidationError: (err) =>\n Effect.succeed(`Handled validation: ${err.message}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `catchTag` lets you recover from a specific tagged error type.\n- `catchTags` lets you handle multiple tagged error types in one place.\n- Unhandled errors continue to propagate, preserving error safety.", - "antiPattern": "Catching all errors generically (e.g., with `catchAll`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators.", - "explanation": "Not all errors should be handled the same way. \nBy matching on specific error tags, you can provide targeted recovery logic for each error type, while letting unhandled errors propagate as needed.", - "content": "# Handling Specific Errors with `catchTag` and `catchTags`\n\n## Guideline\n\nUse the `catchTag` and `catchTags` combinators to recover from or handle specific tagged error types in the Effect failure channel. \nThis enables precise, type-safe error recovery and is especially useful for domain-specific error handling.\n\n## Rationale\n\nNot all errors should be handled the same way. \nBy matching on specific error tags, you can provide targeted recovery logic for each error type, while letting unhandled errors propagate as needed.\n\n## Good Example\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define tagged error types\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{}> {}\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n message: string;\n}> {}\n\ntype MyError = NotFoundError | ValidationError;\n\n// Effect: Handle only ValidationError, let others propagate\nconst effect = Effect.fail(\n new ValidationError({ message: \"Invalid input\" }) as MyError\n).pipe(\n Effect.catchTag(\"ValidationError\", (err) =>\n Effect.succeed(`Recovered from validation error: ${err.message}`)\n )\n); // Effect\n\n// Effect: Handle multiple error tags\nconst effect2 = Effect.fail(new NotFoundError() as MyError).pipe(\n Effect.catchTags({\n NotFoundError: () => Effect.succeed(\"Handled not found!\"),\n ValidationError: (err) =>\n Effect.succeed(`Handled validation: ${err.message}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `catchTag` lets you recover from a specific tagged error type.\n- `catchTags` lets you handle multiple tagged error types in one place.\n- Unhandled errors continue to propagate, preserving error safety.\n\n## Anti-Pattern\n\nCatching all errors generically (e.g., with `catchAll`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators." - }, - { - "id": "getting-started-hello-world", - "title": "Hello World: Your First Effect", - "description": "Create your first Effect program with Effect.succeed.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Step 1: Create an Effect that succeeds with a value\nconst helloWorld = Effect.succeed(\"Hello, Effect!\");\n\n// Step 2: Run the Effect and get the result\nconst result = Effect.runSync(helloWorld);\n\nconsole.log(result); // \"Hello, Effect!\"\n```", - "antiPattern": "", - "explanation": "Every journey starts with \"Hello World\". In Effect, you create computations\nby describing what you want to happen, then you run them. This separation is\nwhat makes Effect powerful.", - "content": "# Hello World: Your First Effect\n\n## Guideline\n\nCreate your first Effect using `Effect.succeed` to wrap a value, then run it\nwith `Effect.runSync` to see the result.\n\n## Rationale\n\nEvery journey starts with \"Hello World\". In Effect, you create computations\nby describing what you want to happen, then you run them. This separation is\nwhat makes Effect powerful.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Step 1: Create an Effect that succeeds with a value\nconst helloWorld = Effect.succeed(\"Hello, Effect!\");\n\n// Step 2: Run the Effect and get the result\nconst result = Effect.runSync(helloWorld);\n\nconsole.log(result); // \"Hello, Effect!\"\n```\n\n## Building Up\n\nNow let's add a side effect with `Effect.log`:\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting my first Effect program...\");\n const message = \"Hello, Effect!\";\n yield* Effect.log(`Message: ${message}`);\n return message;\n});\n\nEffect.runSync(program);\n// Output:\n// timestamp=... level=INFO message=\"Starting my first Effect program...\"\n// timestamp=... level=INFO message=\"Message: Hello, Effect!\"\n```\n\n## Key Concepts\n\n1. **Effect.succeed(value)** - Creates an Effect that always succeeds with the given value\n2. **Effect.log(message)** - Creates an Effect that logs a message\n3. **Effect.gen** - A way to write sequential Effects like async/await\n4. **Effect.runSync** - Executes the Effect and returns the result\n\n## What's Next?\n\n- Learn why Effects are different from Promises\n- Learn how to handle errors with Effect.fail\n- Learn how to transform values with Effect.map" - }, - { - "id": "api-authentication", - "title": "Implement API Authentication", - "description": "Use middleware to validate authentication tokens before handling requests.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "example": "```typescript\nimport { Effect, Context, Layer, Data } from \"effect\"\nimport { HttpServer, HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define authentication types\n// ============================================\n\ninterface User {\n readonly id: string\n readonly email: string\n readonly roles: ReadonlyArray\n}\n\nclass AuthenticatedUser extends Context.Tag(\"AuthenticatedUser\")<\n AuthenticatedUser,\n User\n>() {}\n\nclass UnauthorizedError extends Data.TaggedError(\"UnauthorizedError\")<{\n readonly reason: string\n}> {}\n\nclass ForbiddenError extends Data.TaggedError(\"ForbiddenError\")<{\n readonly requiredRole: string\n}> {}\n\n// ============================================\n// 2. JWT validation service\n// ============================================\n\ninterface JwtService {\n readonly verify: (token: string) => Effect.Effect\n}\n\nclass Jwt extends Context.Tag(\"Jwt\")() {}\n\nconst JwtLive = Layer.succeed(Jwt, {\n verify: (token) =>\n Effect.gen(function* () {\n // In production: use a real JWT library\n if (!token || token === \"invalid\") {\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Invalid or expired token\" \n }))\n }\n\n // Decode token (simplified)\n if (token.startsWith(\"user-\")) {\n return {\n id: token.replace(\"user-\", \"\"),\n email: \"user@example.com\",\n roles: [\"user\"],\n }\n }\n\n if (token.startsWith(\"admin-\")) {\n return {\n id: token.replace(\"admin-\", \"\"),\n email: \"admin@example.com\",\n roles: [\"user\", \"admin\"],\n }\n }\n\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Malformed token\" \n }))\n }),\n})\n\n// ============================================\n// 3. Authentication middleware\n// ============================================\n\nconst extractBearerToken = (header: string | undefined): string | null => {\n if (!header?.startsWith(\"Bearer \")) return null\n return header.slice(7)\n}\n\nconst authenticate = (\n handler: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const jwt = yield* Jwt\n\n const authHeader = request.headers[\"authorization\"]\n const token = extractBearerToken(authHeader)\n\n if (!token) {\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Missing Authorization header\" \n }))\n }\n\n const user = yield* jwt.verify(token)\n\n return yield* handler.pipe(\n Effect.provideService(AuthenticatedUser, user)\n )\n })\n\n// ============================================\n// 4. Role-based authorization\n// ============================================\n\nconst requireRole = (role: string) =>\n (handler: Effect.Effect) =>\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n\n if (!user.roles.includes(role)) {\n return yield* Effect.fail(new ForbiddenError({ requiredRole: role }))\n }\n\n return yield* handler\n })\n\n// ============================================\n// 5. Protected routes\n// ============================================\n\nconst getProfile = authenticate(\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n return HttpServerResponse.json({\n id: user.id,\n email: user.email,\n roles: user.roles,\n })\n })\n)\n\nconst adminOnly = authenticate(\n requireRole(\"admin\")(\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n return HttpServerResponse.json({\n message: `Welcome admin ${user.email}`,\n users: [\"user1\", \"user2\", \"user3\"],\n })\n })\n )\n)\n\n// ============================================\n// 6. Error handling\n// ============================================\n\nconst handleAuthErrors = (effect: Effect.Effect) =>\n effect.pipe(\n Effect.catchTag(\"UnauthorizedError\", (e) =>\n Effect.succeed(\n HttpServerResponse.json({ error: e.reason }, { status: 401 })\n )\n ),\n Effect.catchTag(\"ForbiddenError\", (e) =>\n Effect.succeed(\n HttpServerResponse.json(\n { error: `Requires role: ${e.requiredRole}` },\n { status: 403 }\n )\n )\n )\n )\n```", - "antiPattern": "", - "explanation": "Authentication protects your API:\n\n1. **Identity verification** - Know who's making requests\n2. **Access control** - Limit what users can do\n3. **Audit trail** - Track who did what\n4. **Rate limiting** - Per-user limits\n\n---", - "content": "## Guideline\n\nImplement authentication as middleware that validates tokens and provides user context to route handlers.\n\n---\n\n## Rationale\n\nAuthentication protects your API:\n\n1. **Identity verification** - Know who's making requests\n2. **Access control** - Limit what users can do\n3. **Audit trail** - Track who did what\n4. **Rate limiting** - Per-user limits\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Context, Layer, Data } from \"effect\"\nimport { HttpServer, HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define authentication types\n// ============================================\n\ninterface User {\n readonly id: string\n readonly email: string\n readonly roles: ReadonlyArray\n}\n\nclass AuthenticatedUser extends Context.Tag(\"AuthenticatedUser\")<\n AuthenticatedUser,\n User\n>() {}\n\nclass UnauthorizedError extends Data.TaggedError(\"UnauthorizedError\")<{\n readonly reason: string\n}> {}\n\nclass ForbiddenError extends Data.TaggedError(\"ForbiddenError\")<{\n readonly requiredRole: string\n}> {}\n\n// ============================================\n// 2. JWT validation service\n// ============================================\n\ninterface JwtService {\n readonly verify: (token: string) => Effect.Effect\n}\n\nclass Jwt extends Context.Tag(\"Jwt\")() {}\n\nconst JwtLive = Layer.succeed(Jwt, {\n verify: (token) =>\n Effect.gen(function* () {\n // In production: use a real JWT library\n if (!token || token === \"invalid\") {\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Invalid or expired token\" \n }))\n }\n\n // Decode token (simplified)\n if (token.startsWith(\"user-\")) {\n return {\n id: token.replace(\"user-\", \"\"),\n email: \"user@example.com\",\n roles: [\"user\"],\n }\n }\n\n if (token.startsWith(\"admin-\")) {\n return {\n id: token.replace(\"admin-\", \"\"),\n email: \"admin@example.com\",\n roles: [\"user\", \"admin\"],\n }\n }\n\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Malformed token\" \n }))\n }),\n})\n\n// ============================================\n// 3. Authentication middleware\n// ============================================\n\nconst extractBearerToken = (header: string | undefined): string | null => {\n if (!header?.startsWith(\"Bearer \")) return null\n return header.slice(7)\n}\n\nconst authenticate = (\n handler: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n const jwt = yield* Jwt\n\n const authHeader = request.headers[\"authorization\"]\n const token = extractBearerToken(authHeader)\n\n if (!token) {\n return yield* Effect.fail(new UnauthorizedError({ \n reason: \"Missing Authorization header\" \n }))\n }\n\n const user = yield* jwt.verify(token)\n\n return yield* handler.pipe(\n Effect.provideService(AuthenticatedUser, user)\n )\n })\n\n// ============================================\n// 4. Role-based authorization\n// ============================================\n\nconst requireRole = (role: string) =>\n (handler: Effect.Effect) =>\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n\n if (!user.roles.includes(role)) {\n return yield* Effect.fail(new ForbiddenError({ requiredRole: role }))\n }\n\n return yield* handler\n })\n\n// ============================================\n// 5. Protected routes\n// ============================================\n\nconst getProfile = authenticate(\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n return HttpServerResponse.json({\n id: user.id,\n email: user.email,\n roles: user.roles,\n })\n })\n)\n\nconst adminOnly = authenticate(\n requireRole(\"admin\")(\n Effect.gen(function* () {\n const user = yield* AuthenticatedUser\n return HttpServerResponse.json({\n message: `Welcome admin ${user.email}`,\n users: [\"user1\", \"user2\", \"user3\"],\n })\n })\n )\n)\n\n// ============================================\n// 6. Error handling\n// ============================================\n\nconst handleAuthErrors = (effect: Effect.Effect) =>\n effect.pipe(\n Effect.catchTag(\"UnauthorizedError\", (e) =>\n Effect.succeed(\n HttpServerResponse.json({ error: e.reason }, { status: 401 })\n )\n ),\n Effect.catchTag(\"ForbiddenError\", (e) =>\n Effect.succeed(\n HttpServerResponse.json(\n { error: `Requires role: ${e.requiredRole}` },\n { status: 403 }\n )\n )\n )\n )\n```\n\n## Authentication Flow\n\n```\nRequest → Extract Token → Verify JWT → Provide User → Handler\n ↓ ↓\n 401 401/403\n```\n\n## Token Types\n\n| Type | Storage | Best For |\n|------|---------|----------|\n| JWT | Header | Stateless APIs |\n| Session | Cookie | Web apps |\n| API Key | Header | Service-to-service |\n\n## Best Practices\n\n1. **Use HTTPS** - Tokens in plain HTTP are visible\n2. **Short expiry** - JWTs should expire quickly\n3. **Refresh tokens** - For long sessions\n4. **Validate claims** - Check issuer, audience, expiry\n5. **Log auth events** - Track login attempts" - }, - { - "id": "pipeline-backpressure", - "title": "Implement Backpressure in Pipelines", - "description": "Use buffering and throttling to handle producers faster than consumers.", - "skillLevel": "advanced", - "useCases": [ - "building-data-pipelines" - ], - "example": "```typescript\nimport { Effect, Stream, Schedule, Duration, Queue, Chunk } from \"effect\"\n\n// ============================================\n// 1. Stream with natural backpressure\n// ============================================\n\n// Streams have built-in backpressure - consumers pull data\nconst fastProducer = Stream.fromIterable(Array.from({ length: 1000 }, (_, i) => i))\n\nconst slowConsumer = fastProducer.pipe(\n Stream.tap((n) =>\n Effect.gen(function* () {\n yield* Effect.sleep(\"10 millis\") // Slow processing\n yield* Effect.log(`Processed: ${n}`)\n })\n ),\n Stream.runDrain\n)\n\n// Producer automatically slows down to match consumer\n\n// ============================================\n// 2. Explicit buffer with drop strategy\n// ============================================\n\nconst bufferedStream = (source: Stream.Stream) =>\n source.pipe(\n // Buffer up to 100 items, drop oldest when full\n Stream.buffer({ capacity: 100, strategy: \"dropping\" })\n )\n\n// ============================================\n// 3. Throttling - limit rate\n// ============================================\n\nconst throttledStream = (source: Stream.Stream) =>\n source.pipe(\n // Process at most 10 items per second\n Stream.throttle({\n cost: () => 1,\n units: 10,\n duration: \"1 second\",\n strategy: \"enforce\",\n })\n )\n\n// ============================================\n// 4. Debounce - wait for quiet period\n// ============================================\n\nconst debouncedStream = (source: Stream.Stream) =>\n source.pipe(\n // Wait 100ms of no new items before emitting\n Stream.debounce(\"100 millis\")\n )\n\n// ============================================\n// 5. Bounded queue for producer-consumer\n// ============================================\n\nconst boundedQueueExample = Effect.gen(function* () {\n // Create bounded queue - blocks producer when full\n const queue = yield* Queue.bounded(10)\n\n // Fast producer\n const producer = Effect.gen(function* () {\n for (let i = 0; i < 100; i++) {\n yield* Queue.offer(queue, i)\n yield* Effect.log(`Produced: ${i}`)\n }\n yield* Queue.shutdown(queue)\n })\n\n // Slow consumer\n const consumer = Effect.gen(function* () {\n let count = 0\n while (true) {\n const item = yield* Queue.take(queue).pipe(\n Effect.catchTag(\"QueueShutdown\", () => Effect.fail(\"done\" as const))\n )\n if (item === \"done\") break\n yield* Effect.sleep(\"50 millis\") // Slow processing\n yield* Effect.log(`Consumed: ${item}`)\n count++\n }\n return count\n }).pipe(Effect.catchAll(() => Effect.succeed(0)))\n\n // Run both - producer will block when queue is full\n yield* Effect.all([producer, consumer], { concurrency: 2 })\n})\n\n// ============================================\n// 6. Sliding window - keep most recent\n// ============================================\n\nconst slidingWindowStream = (source: Stream.Stream) =>\n source.pipe(\n Stream.sliding(5), // Keep last 5 items\n Stream.map((window) => ({\n items: window,\n average: Chunk.reduce(window, 0, (a, b) => a + b) / Chunk.size(window),\n }))\n )\n\n// ============================================\n// 7. Run example\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Backpressure Demo ===\")\n\n // Throttled stream\n const throttled = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).pipe(\n Stream.tap((n) => Effect.log(`Emitting: ${n}`)),\n Stream.throttle({\n cost: () => 1,\n units: 2,\n duration: \"1 second\",\n strategy: \"enforce\",\n }),\n Stream.tap((n) => Effect.log(`After throttle: ${n}`)),\n Stream.runDrain\n )\n\n yield* throttled\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Backpressure prevents system overload:\n\n1. **Memory safety** - Don't buffer unlimited data\n2. **Stability** - Slow consumers don't crash\n3. **Fairness** - Distribute load appropriately\n4. **Predictability** - Consistent performance\n\n---", - "content": "## Guideline\n\nUse Stream's built-in backpressure mechanisms and explicit buffering to handle situations where data producers are faster than consumers.\n\n---\n\n## Rationale\n\nBackpressure prevents system overload:\n\n1. **Memory safety** - Don't buffer unlimited data\n2. **Stability** - Slow consumers don't crash\n3. **Fairness** - Distribute load appropriately\n4. **Predictability** - Consistent performance\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Schedule, Duration, Queue, Chunk } from \"effect\"\n\n// ============================================\n// 1. Stream with natural backpressure\n// ============================================\n\n// Streams have built-in backpressure - consumers pull data\nconst fastProducer = Stream.fromIterable(Array.from({ length: 1000 }, (_, i) => i))\n\nconst slowConsumer = fastProducer.pipe(\n Stream.tap((n) =>\n Effect.gen(function* () {\n yield* Effect.sleep(\"10 millis\") // Slow processing\n yield* Effect.log(`Processed: ${n}`)\n })\n ),\n Stream.runDrain\n)\n\n// Producer automatically slows down to match consumer\n\n// ============================================\n// 2. Explicit buffer with drop strategy\n// ============================================\n\nconst bufferedStream = (source: Stream.Stream) =>\n source.pipe(\n // Buffer up to 100 items, drop oldest when full\n Stream.buffer({ capacity: 100, strategy: \"dropping\" })\n )\n\n// ============================================\n// 3. Throttling - limit rate\n// ============================================\n\nconst throttledStream = (source: Stream.Stream) =>\n source.pipe(\n // Process at most 10 items per second\n Stream.throttle({\n cost: () => 1,\n units: 10,\n duration: \"1 second\",\n strategy: \"enforce\",\n })\n )\n\n// ============================================\n// 4. Debounce - wait for quiet period\n// ============================================\n\nconst debouncedStream = (source: Stream.Stream) =>\n source.pipe(\n // Wait 100ms of no new items before emitting\n Stream.debounce(\"100 millis\")\n )\n\n// ============================================\n// 5. Bounded queue for producer-consumer\n// ============================================\n\nconst boundedQueueExample = Effect.gen(function* () {\n // Create bounded queue - blocks producer when full\n const queue = yield* Queue.bounded(10)\n\n // Fast producer\n const producer = Effect.gen(function* () {\n for (let i = 0; i < 100; i++) {\n yield* Queue.offer(queue, i)\n yield* Effect.log(`Produced: ${i}`)\n }\n yield* Queue.shutdown(queue)\n })\n\n // Slow consumer\n const consumer = Effect.gen(function* () {\n let count = 0\n while (true) {\n const item = yield* Queue.take(queue).pipe(\n Effect.catchTag(\"QueueShutdown\", () => Effect.fail(\"done\" as const))\n )\n if (item === \"done\") break\n yield* Effect.sleep(\"50 millis\") // Slow processing\n yield* Effect.log(`Consumed: ${item}`)\n count++\n }\n return count\n }).pipe(Effect.catchAll(() => Effect.succeed(0)))\n\n // Run both - producer will block when queue is full\n yield* Effect.all([producer, consumer], { concurrency: 2 })\n})\n\n// ============================================\n// 6. Sliding window - keep most recent\n// ============================================\n\nconst slidingWindowStream = (source: Stream.Stream) =>\n source.pipe(\n Stream.sliding(5), // Keep last 5 items\n Stream.map((window) => ({\n items: window,\n average: Chunk.reduce(window, 0, (a, b) => a + b) / Chunk.size(window),\n }))\n )\n\n// ============================================\n// 7. Run example\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Backpressure Demo ===\")\n\n // Throttled stream\n const throttled = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).pipe(\n Stream.tap((n) => Effect.log(`Emitting: ${n}`)),\n Stream.throttle({\n cost: () => 1,\n units: 2,\n duration: \"1 second\",\n strategy: \"enforce\",\n }),\n Stream.tap((n) => Effect.log(`After throttle: ${n}`)),\n Stream.runDrain\n )\n\n yield* throttled\n})\n\nEffect.runPromise(program)\n```\n\n## Backpressure Strategies\n\n| Strategy | Behavior | Use When |\n|----------|----------|----------|\n| **Block** | Producer waits | Data loss unacceptable |\n| **Drop oldest** | Discard old items | Real-time data |\n| **Drop newest** | Discard new items | Batch processing |\n| **Throttle** | Limit rate | API rate limits |\n| **Sample** | Keep periodic | High-frequency data |\n\n## Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `Stream.buffer` | Add explicit buffer |\n| `Stream.throttle` | Limit items per time |\n| `Stream.debounce` | Wait for quiet period |\n| `Queue.bounded` | Bounded queue |\n| `Stream.sliding` | Moving window |\n\n## Best Practices\n\n1. **Size buffers appropriately** - Too small = blocking, too large = memory\n2. **Monitor queue depth** - Alerts when consistently full\n3. **Choose strategy per use case** - Real-time vs batch\n4. **Test under load** - Verify behavior at limits" - }, - { - "id": "pipeline-dead-letter-queue", - "title": "Implement Dead Letter Queues", - "description": "Capture failed items with context for debugging and retry instead of losing them.", - "skillLevel": "advanced", - "useCases": [ - "building-data-pipelines" - ], - "example": "```typescript\nimport { Effect, Stream, Queue, Chunk, Ref, Data } from \"effect\"\n\n// ============================================\n// 1. Define DLQ types\n// ============================================\n\ninterface DeadLetterItem {\n readonly item: T\n readonly error: unknown\n readonly timestamp: Date\n readonly attempts: number\n readonly context: Record\n}\n\ninterface ProcessingResult {\n readonly _tag: \"Success\" | \"Failure\"\n}\n\nclass Success implements ProcessingResult {\n readonly _tag = \"Success\"\n constructor(\n readonly item: T,\n readonly result: R\n ) {}\n}\n\nclass Failure implements ProcessingResult {\n readonly _tag = \"Failure\"\n constructor(\n readonly item: T,\n readonly error: unknown,\n readonly attempts: number\n ) {}\n}\n\n// ============================================\n// 2. Create a DLQ service\n// ============================================\n\nconst makeDLQ = () =>\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded>()\n const countRef = yield* Ref.make(0)\n\n return {\n send: (item: T, error: unknown, attempts: number, context: Record = {}) =>\n Effect.gen(function* () {\n yield* Queue.offer(queue, {\n item,\n error,\n timestamp: new Date(),\n attempts,\n context,\n })\n yield* Ref.update(countRef, (n) => n + 1)\n yield* Effect.log(`DLQ: Added item (total: ${(yield* Ref.get(countRef))})`)\n }),\n\n getAll: () =>\n Effect.gen(function* () {\n const items: DeadLetterItem[] = []\n while (!(yield* Queue.isEmpty(queue))) {\n const item = yield* Queue.poll(queue)\n if (item._tag === \"Some\") {\n items.push(item.value)\n }\n }\n return items\n }),\n\n count: () => Ref.get(countRef),\n\n queue,\n }\n })\n\n// ============================================\n// 3. Process with DLQ\n// ============================================\n\ninterface Order {\n id: string\n amount: number\n}\n\nconst processOrder = (order: Order): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate random failures\n if (order.amount < 0) {\n return yield* Effect.fail(new Error(\"Invalid amount\"))\n }\n if (order.id === \"fail\") {\n return yield* Effect.fail(new Error(\"Processing failed\"))\n }\n yield* Effect.sleep(\"10 millis\")\n return `Processed order ${order.id}: $${order.amount}`\n })\n\nconst processWithRetryAndDLQ = (\n orders: Stream.Stream,\n maxRetries: number = 3\n) =>\n Effect.gen(function* () {\n const dlq = yield* makeDLQ()\n\n const results = yield* orders.pipe(\n Stream.mapEffect((order) =>\n Effect.gen(function* () {\n let lastError: unknown\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n const result = yield* processOrder(order).pipe(\n Effect.map((r) => new Success(order, r)),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Attempt ${attempt}/${maxRetries} failed for ${order.id}`)\n lastError = error\n if (attempt < maxRetries) {\n yield* Effect.sleep(\"100 millis\") // Backoff\n }\n return new Failure(order, error, attempt) as ProcessingResult\n })\n )\n )\n\n if (result._tag === \"Success\") {\n return result\n }\n }\n\n // All retries exhausted - send to DLQ\n yield* dlq.send(order, lastError, maxRetries, { orderId: order.id })\n return new Failure(order, lastError, maxRetries)\n })\n ),\n Stream.runCollect\n )\n\n const successful = Chunk.filter(results, (r): r is Success => r._tag === \"Success\")\n const failed = Chunk.filter(results, (r): r is Failure => r._tag === \"Failure\")\n\n yield* Effect.log(`\\nResults: ${Chunk.size(successful)} success, ${Chunk.size(failed)} failed`)\n\n // Get DLQ contents\n const dlqItems = yield* dlq.getAll()\n if (dlqItems.length > 0) {\n yield* Effect.log(\"\\n=== Dead Letter Queue Contents ===\")\n for (const item of dlqItems) {\n yield* Effect.log(\n `- Order ${item.item.id}: ${item.error} (attempts: ${item.attempts})`\n )\n }\n }\n\n return { successful, failed, dlqItems }\n })\n\n// ============================================\n// 4. DLQ reprocessing\n// ============================================\n\nconst reprocessDLQ = (\n dlqItems: DeadLetterItem[],\n processor: (item: T) => Effect.Effect\n) =>\n Effect.gen(function* () {\n yield* Effect.log(`Reprocessing ${dlqItems.length} DLQ items...`)\n\n for (const dlqItem of dlqItems) {\n const result = yield* processor(dlqItem.item).pipe(\n Effect.map(() => \"success\" as const),\n Effect.catchAll(() => Effect.succeed(\"failed\" as const))\n )\n\n yield* Effect.log(\n `Reprocess ${JSON.stringify(dlqItem.item)}: ${result}`\n )\n }\n })\n\n// ============================================\n// 5. Run example\n// ============================================\n\nconst program = Effect.gen(function* () {\n const orders: Order[] = [\n { id: \"1\", amount: 100 },\n { id: \"2\", amount: 200 },\n { id: \"fail\", amount: 50 }, // Will fail all retries\n { id: \"3\", amount: 300 },\n { id: \"4\", amount: -10 }, // Invalid amount\n { id: \"5\", amount: 150 },\n ]\n\n yield* Effect.log(\"=== Processing Orders ===\\n\")\n const { dlqItems } = yield* processWithRetryAndDLQ(Stream.fromIterable(orders), 3)\n\n if (dlqItems.length > 0) {\n yield* Effect.log(\"\\n=== Attempting DLQ Reprocessing ===\")\n yield* reprocessDLQ(dlqItems, (order) =>\n Effect.gen(function* () {\n yield* Effect.log(`Manual fix for order ${order.id}`)\n })\n )\n }\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Dead letter queues provide:\n\n1. **Resilience** - Pipeline continues despite failures\n2. **Visibility** - See what's failing and why\n3. **Recovery** - Reprocess failed items later\n4. **Debugging** - Error context for investigation\n\n---", - "content": "## Guideline\n\nRoute items that fail processing to a dead letter queue (DLQ) with error context, allowing the main pipeline to continue while preserving failed items for investigation.\n\n---\n\n## Rationale\n\nDead letter queues provide:\n\n1. **Resilience** - Pipeline continues despite failures\n2. **Visibility** - See what's failing and why\n3. **Recovery** - Reprocess failed items later\n4. **Debugging** - Error context for investigation\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Queue, Chunk, Ref, Data } from \"effect\"\n\n// ============================================\n// 1. Define DLQ types\n// ============================================\n\ninterface DeadLetterItem {\n readonly item: T\n readonly error: unknown\n readonly timestamp: Date\n readonly attempts: number\n readonly context: Record\n}\n\ninterface ProcessingResult {\n readonly _tag: \"Success\" | \"Failure\"\n}\n\nclass Success implements ProcessingResult {\n readonly _tag = \"Success\"\n constructor(\n readonly item: T,\n readonly result: R\n ) {}\n}\n\nclass Failure implements ProcessingResult {\n readonly _tag = \"Failure\"\n constructor(\n readonly item: T,\n readonly error: unknown,\n readonly attempts: number\n ) {}\n}\n\n// ============================================\n// 2. Create a DLQ service\n// ============================================\n\nconst makeDLQ = () =>\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded>()\n const countRef = yield* Ref.make(0)\n\n return {\n send: (item: T, error: unknown, attempts: number, context: Record = {}) =>\n Effect.gen(function* () {\n yield* Queue.offer(queue, {\n item,\n error,\n timestamp: new Date(),\n attempts,\n context,\n })\n yield* Ref.update(countRef, (n) => n + 1)\n yield* Effect.log(`DLQ: Added item (total: ${(yield* Ref.get(countRef))})`)\n }),\n\n getAll: () =>\n Effect.gen(function* () {\n const items: DeadLetterItem[] = []\n while (!(yield* Queue.isEmpty(queue))) {\n const item = yield* Queue.poll(queue)\n if (item._tag === \"Some\") {\n items.push(item.value)\n }\n }\n return items\n }),\n\n count: () => Ref.get(countRef),\n\n queue,\n }\n })\n\n// ============================================\n// 3. Process with DLQ\n// ============================================\n\ninterface Order {\n id: string\n amount: number\n}\n\nconst processOrder = (order: Order): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate random failures\n if (order.amount < 0) {\n return yield* Effect.fail(new Error(\"Invalid amount\"))\n }\n if (order.id === \"fail\") {\n return yield* Effect.fail(new Error(\"Processing failed\"))\n }\n yield* Effect.sleep(\"10 millis\")\n return `Processed order ${order.id}: $${order.amount}`\n })\n\nconst processWithRetryAndDLQ = (\n orders: Stream.Stream,\n maxRetries: number = 3\n) =>\n Effect.gen(function* () {\n const dlq = yield* makeDLQ()\n\n const results = yield* orders.pipe(\n Stream.mapEffect((order) =>\n Effect.gen(function* () {\n let lastError: unknown\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n const result = yield* processOrder(order).pipe(\n Effect.map((r) => new Success(order, r)),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`Attempt ${attempt}/${maxRetries} failed for ${order.id}`)\n lastError = error\n if (attempt < maxRetries) {\n yield* Effect.sleep(\"100 millis\") // Backoff\n }\n return new Failure(order, error, attempt) as ProcessingResult\n })\n )\n )\n\n if (result._tag === \"Success\") {\n return result\n }\n }\n\n // All retries exhausted - send to DLQ\n yield* dlq.send(order, lastError, maxRetries, { orderId: order.id })\n return new Failure(order, lastError, maxRetries)\n })\n ),\n Stream.runCollect\n )\n\n const successful = Chunk.filter(results, (r): r is Success => r._tag === \"Success\")\n const failed = Chunk.filter(results, (r): r is Failure => r._tag === \"Failure\")\n\n yield* Effect.log(`\\nResults: ${Chunk.size(successful)} success, ${Chunk.size(failed)} failed`)\n\n // Get DLQ contents\n const dlqItems = yield* dlq.getAll()\n if (dlqItems.length > 0) {\n yield* Effect.log(\"\\n=== Dead Letter Queue Contents ===\")\n for (const item of dlqItems) {\n yield* Effect.log(\n `- Order ${item.item.id}: ${item.error} (attempts: ${item.attempts})`\n )\n }\n }\n\n return { successful, failed, dlqItems }\n })\n\n// ============================================\n// 4. DLQ reprocessing\n// ============================================\n\nconst reprocessDLQ = (\n dlqItems: DeadLetterItem[],\n processor: (item: T) => Effect.Effect\n) =>\n Effect.gen(function* () {\n yield* Effect.log(`Reprocessing ${dlqItems.length} DLQ items...`)\n\n for (const dlqItem of dlqItems) {\n const result = yield* processor(dlqItem.item).pipe(\n Effect.map(() => \"success\" as const),\n Effect.catchAll(() => Effect.succeed(\"failed\" as const))\n )\n\n yield* Effect.log(\n `Reprocess ${JSON.stringify(dlqItem.item)}: ${result}`\n )\n }\n })\n\n// ============================================\n// 5. Run example\n// ============================================\n\nconst program = Effect.gen(function* () {\n const orders: Order[] = [\n { id: \"1\", amount: 100 },\n { id: \"2\", amount: 200 },\n { id: \"fail\", amount: 50 }, // Will fail all retries\n { id: \"3\", amount: 300 },\n { id: \"4\", amount: -10 }, // Invalid amount\n { id: \"5\", amount: 150 },\n ]\n\n yield* Effect.log(\"=== Processing Orders ===\\n\")\n const { dlqItems } = yield* processWithRetryAndDLQ(Stream.fromIterable(orders), 3)\n\n if (dlqItems.length > 0) {\n yield* Effect.log(\"\\n=== Attempting DLQ Reprocessing ===\")\n yield* reprocessDLQ(dlqItems, (order) =>\n Effect.gen(function* () {\n yield* Effect.log(`Manual fix for order ${order.id}`)\n })\n )\n }\n})\n\nEffect.runPromise(program)\n```\n\n## DLQ Strategies\n\n| Strategy | Use Case |\n|----------|----------|\n| **Immediate DLQ** | After first failure |\n| **Retry then DLQ** | After N retries |\n| **Timed DLQ** | After timeout |\n| **Conditional DLQ** | Based on error type |\n\n## DLQ Item Contents\n\n| Field | Purpose |\n|-------|---------|\n| `item` | Original data |\n| `error` | What went wrong |\n| `timestamp` | When it failed |\n| `attempts` | Retry count |\n| `context` | Debug info |\n\n## Best Practices\n\n1. **Include context** - Error messages, stack traces\n2. **Set TTL** - Don't keep forever\n3. **Alert on growth** - DLQ size indicates issues\n4. **Automate reprocessing** - Where safe\n5. **Audit trail** - Track reprocessing attempts" - }, - { - "id": "observability-distributed-tracing", - "title": "Implement Distributed Tracing", - "description": "Propagate trace context across service boundaries to correlate requests.", - "skillLevel": "advanced", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, Context, Layer } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define trace context\n// ============================================\n\ninterface TraceContext {\n readonly traceId: string\n readonly spanId: string\n readonly parentSpanId?: string\n readonly sampled: boolean\n}\n\nclass CurrentTrace extends Context.Tag(\"CurrentTrace\")<\n CurrentTrace,\n TraceContext\n>() {}\n\n// W3C Trace Context header names\nconst TRACEPARENT_HEADER = \"traceparent\"\nconst TRACESTATE_HEADER = \"tracestate\"\n\n// ============================================\n// 2. Generate trace IDs\n// ============================================\n\nconst generateTraceId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(16)))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n\nconst generateSpanId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(8)))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n\n// ============================================\n// 3. Parse and format trace context\n// ============================================\n\nconst parseTraceparent = (header: string): TraceContext | null => {\n // Format: 00-traceId-spanId-flags\n const parts = header.split(\"-\")\n if (parts.length !== 4) return null\n\n return {\n traceId: parts[1],\n spanId: generateSpanId(), // New span for this service\n parentSpanId: parts[2],\n sampled: parts[3] === \"01\",\n }\n}\n\nconst formatTraceparent = (ctx: TraceContext): string =>\n `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? \"01\" : \"00\"}`\n\n// ============================================\n// 4. Extract trace from incoming request\n// ============================================\n\nconst extractTraceContext = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n\n const traceparent = request.headers[TRACEPARENT_HEADER]\n\n if (traceparent) {\n const parsed = parseTraceparent(traceparent)\n if (parsed) {\n yield* Effect.log(\"Extracted trace context\").pipe(\n Effect.annotateLogs({\n traceId: parsed.traceId,\n parentSpanId: parsed.parentSpanId,\n })\n )\n return parsed\n }\n }\n\n // No incoming trace - start a new one\n const newTrace: TraceContext = {\n traceId: generateTraceId(),\n spanId: generateSpanId(),\n sampled: Math.random() < 0.1, // 10% sampling\n }\n\n yield* Effect.log(\"Started new trace\").pipe(\n Effect.annotateLogs({ traceId: newTrace.traceId })\n )\n\n return newTrace\n})\n\n// ============================================\n// 5. Propagate trace to outgoing requests\n// ============================================\n\nconst makeTracedHttpClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n const trace = yield* CurrentTrace\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n // Create child span for outgoing request\n const childSpan: TraceContext = {\n traceId: trace.traceId,\n spanId: generateSpanId(),\n parentSpanId: trace.spanId,\n sampled: trace.sampled,\n }\n\n yield* Effect.log(\"Making traced HTTP request\").pipe(\n Effect.annotateLogs({\n traceId: childSpan.traceId,\n spanId: childSpan.spanId,\n url,\n })\n )\n\n const request = HttpClientRequest.get(url).pipe(\n HttpClientRequest.setHeader(\n TRACEPARENT_HEADER,\n formatTraceparent(childSpan)\n )\n )\n\n return yield* baseClient.execute(request)\n }),\n }\n})\n\n// ============================================\n// 6. Tracing middleware for HTTP server\n// ============================================\n\nconst withTracing = (\n handler: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const traceContext = yield* extractTraceContext\n\n return yield* handler.pipe(\n Effect.provideService(CurrentTrace, traceContext),\n Effect.withLogSpan(`request-${traceContext.spanId}`),\n Effect.annotateLogs({\n \"trace.id\": traceContext.traceId,\n \"span.id\": traceContext.spanId,\n \"parent.span.id\": traceContext.parentSpanId ?? \"none\",\n })\n )\n })\n\n// ============================================\n// 7. Example: Service A calls Service B\n// ============================================\n\n// Service B handler\nconst serviceBHandler = withTracing(\n Effect.gen(function* () {\n const trace = yield* CurrentTrace\n yield* Effect.log(\"Service B processing request\")\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\")\n\n return HttpServerResponse.json({\n message: \"Hello from Service B\",\n traceId: trace.traceId,\n })\n })\n)\n\n// Service A handler (calls Service B)\nconst serviceAHandler = withTracing(\n Effect.gen(function* () {\n const trace = yield* CurrentTrace\n yield* Effect.log(\"Service A processing request\")\n\n // Call Service B with trace propagation\n const tracedClient = yield* makeTracedHttpClient\n const response = yield* tracedClient.get(\"http://service-b/api/data\")\n\n yield* Effect.log(\"Service A received response from B\")\n\n return HttpServerResponse.json({\n message: \"Hello from Service A\",\n traceId: trace.traceId,\n })\n })\n)\n\n// ============================================\n// 8. Run and observe\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Distributed Tracing Demo ===\")\n\n // Simulate incoming request with trace\n const incomingTrace: TraceContext = {\n traceId: generateTraceId(),\n spanId: generateSpanId(),\n sampled: true,\n }\n\n yield* Effect.log(\"Processing traced request\").pipe(\n Effect.provideService(CurrentTrace, incomingTrace),\n Effect.annotateLogs({\n \"trace.id\": incomingTrace.traceId,\n \"span.id\": incomingTrace.spanId,\n })\n )\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Distributed tracing shows the complete request journey:\n\n1. **End-to-end visibility** - See entire request flow\n2. **Latency analysis** - Find slow services\n3. **Error correlation** - Link errors across services\n4. **Dependency mapping** - Understand service relationships\n\n---", - "content": "## Guideline\n\nImplement distributed tracing by propagating trace context through HTTP headers and using consistent span naming across services.\n\n---\n\n## Rationale\n\nDistributed tracing shows the complete request journey:\n\n1. **End-to-end visibility** - See entire request flow\n2. **Latency analysis** - Find slow services\n3. **Error correlation** - Link errors across services\n4. **Dependency mapping** - Understand service relationships\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Context, Layer } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Define trace context\n// ============================================\n\ninterface TraceContext {\n readonly traceId: string\n readonly spanId: string\n readonly parentSpanId?: string\n readonly sampled: boolean\n}\n\nclass CurrentTrace extends Context.Tag(\"CurrentTrace\")<\n CurrentTrace,\n TraceContext\n>() {}\n\n// W3C Trace Context header names\nconst TRACEPARENT_HEADER = \"traceparent\"\nconst TRACESTATE_HEADER = \"tracestate\"\n\n// ============================================\n// 2. Generate trace IDs\n// ============================================\n\nconst generateTraceId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(16)))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n\nconst generateSpanId = (): string =>\n Array.from(crypto.getRandomValues(new Uint8Array(8)))\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\")\n\n// ============================================\n// 3. Parse and format trace context\n// ============================================\n\nconst parseTraceparent = (header: string): TraceContext | null => {\n // Format: 00-traceId-spanId-flags\n const parts = header.split(\"-\")\n if (parts.length !== 4) return null\n\n return {\n traceId: parts[1],\n spanId: generateSpanId(), // New span for this service\n parentSpanId: parts[2],\n sampled: parts[3] === \"01\",\n }\n}\n\nconst formatTraceparent = (ctx: TraceContext): string =>\n `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? \"01\" : \"00\"}`\n\n// ============================================\n// 4. Extract trace from incoming request\n// ============================================\n\nconst extractTraceContext = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest\n\n const traceparent = request.headers[TRACEPARENT_HEADER]\n\n if (traceparent) {\n const parsed = parseTraceparent(traceparent)\n if (parsed) {\n yield* Effect.log(\"Extracted trace context\").pipe(\n Effect.annotateLogs({\n traceId: parsed.traceId,\n parentSpanId: parsed.parentSpanId,\n })\n )\n return parsed\n }\n }\n\n // No incoming trace - start a new one\n const newTrace: TraceContext = {\n traceId: generateTraceId(),\n spanId: generateSpanId(),\n sampled: Math.random() < 0.1, // 10% sampling\n }\n\n yield* Effect.log(\"Started new trace\").pipe(\n Effect.annotateLogs({ traceId: newTrace.traceId })\n )\n\n return newTrace\n})\n\n// ============================================\n// 5. Propagate trace to outgoing requests\n// ============================================\n\nconst makeTracedHttpClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n const trace = yield* CurrentTrace\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n // Create child span for outgoing request\n const childSpan: TraceContext = {\n traceId: trace.traceId,\n spanId: generateSpanId(),\n parentSpanId: trace.spanId,\n sampled: trace.sampled,\n }\n\n yield* Effect.log(\"Making traced HTTP request\").pipe(\n Effect.annotateLogs({\n traceId: childSpan.traceId,\n spanId: childSpan.spanId,\n url,\n })\n )\n\n const request = HttpClientRequest.get(url).pipe(\n HttpClientRequest.setHeader(\n TRACEPARENT_HEADER,\n formatTraceparent(childSpan)\n )\n )\n\n return yield* baseClient.execute(request)\n }),\n }\n})\n\n// ============================================\n// 6. Tracing middleware for HTTP server\n// ============================================\n\nconst withTracing = (\n handler: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const traceContext = yield* extractTraceContext\n\n return yield* handler.pipe(\n Effect.provideService(CurrentTrace, traceContext),\n Effect.withLogSpan(`request-${traceContext.spanId}`),\n Effect.annotateLogs({\n \"trace.id\": traceContext.traceId,\n \"span.id\": traceContext.spanId,\n \"parent.span.id\": traceContext.parentSpanId ?? \"none\",\n })\n )\n })\n\n// ============================================\n// 7. Example: Service A calls Service B\n// ============================================\n\n// Service B handler\nconst serviceBHandler = withTracing(\n Effect.gen(function* () {\n const trace = yield* CurrentTrace\n yield* Effect.log(\"Service B processing request\")\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\")\n\n return HttpServerResponse.json({\n message: \"Hello from Service B\",\n traceId: trace.traceId,\n })\n })\n)\n\n// Service A handler (calls Service B)\nconst serviceAHandler = withTracing(\n Effect.gen(function* () {\n const trace = yield* CurrentTrace\n yield* Effect.log(\"Service A processing request\")\n\n // Call Service B with trace propagation\n const tracedClient = yield* makeTracedHttpClient\n const response = yield* tracedClient.get(\"http://service-b/api/data\")\n\n yield* Effect.log(\"Service A received response from B\")\n\n return HttpServerResponse.json({\n message: \"Hello from Service A\",\n traceId: trace.traceId,\n })\n })\n)\n\n// ============================================\n// 8. Run and observe\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Distributed Tracing Demo ===\")\n\n // Simulate incoming request with trace\n const incomingTrace: TraceContext = {\n traceId: generateTraceId(),\n spanId: generateSpanId(),\n sampled: true,\n }\n\n yield* Effect.log(\"Processing traced request\").pipe(\n Effect.provideService(CurrentTrace, incomingTrace),\n Effect.annotateLogs({\n \"trace.id\": incomingTrace.traceId,\n \"span.id\": incomingTrace.spanId,\n })\n )\n})\n\nEffect.runPromise(program)\n```\n\n## Trace Context Propagation\n\n```\n┌─────────────┐ traceparent ┌─────────────┐\n│ Service A │ ───────────────────► │ Service B │\n│ span: abc │ 00-xyz-abc-01 │ span: def │\n└─────────────┘ └─────────────┘\n │ │\n │ traceparent │\n │ ◄─────────────────────────────────┘\n │ 00-xyz-def-01\n```\n\n## W3C Trace Context Format\n\n```\ntraceparent: 00-{traceId}-{spanId}-{flags}\n | 32 hex 8 hex 2 hex\n version 01=sampled\n```\n\n## Best Practices\n\n1. **Use W3C standard** - Interop with other systems\n2. **Sample appropriately** - 100% in dev, 1-10% in prod\n3. **Include trace in logs** - Correlate logs with traces\n4. **Propagate across all boundaries** - HTTP, queues, async\n5. **Use consistent span names** - `service.operation`" - }, - { - "id": "implement-graceful-shutdown", - "title": "Implement Graceful Shutdown for Your Application", - "description": "Use Effect.runFork and OS signal listeners to implement graceful shutdown for long-running applications.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This example creates a server with a \"scoped\" database connection. It uses `runFork` to start the server and sets up a `SIGINT` handler to interrupt the server fiber, which in turn guarantees the database finalizer is called.\n\n```typescript\nimport { Effect, Layer, Fiber, Context, Scope } from \"effect\";\nimport * as http from \"http\";\n\n// 1. A service with a finalizer for cleanup\nclass Database extends Effect.Service()(\"Database\", {\n effect: Effect.gen(function* () {\n yield* Effect.log(\"Acquiring DB connection\");\n return {\n query: () => Effect.succeed(\"data\"),\n };\n }),\n}) {}\n\n// 2. The main server logic\nconst server = Effect.gen(function* () {\n const db = yield* Database;\n\n // Create server with proper error handling\n const httpServer = yield* Effect.sync(() => {\n const server = http.createServer((_req, res) => {\n Effect.runFork(\n Effect.provide(\n db.query().pipe(Effect.map((data) => res.end(data))),\n Database.Default\n )\n );\n });\n return server;\n });\n\n // Add a finalizer to close the server\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n httpServer.close();\n yield* Effect.log(\"Server closed\");\n })\n );\n\n // Start server with error handling\n yield* Effect.async((resume) => {\n httpServer.once(\"error\", (err: Error) => {\n resume(Effect.fail(new Error(`Failed to start server: ${err.message}`)));\n });\n\n httpServer.listen(3456, () => {\n resume(Effect.succeed(void 0));\n });\n });\n\n yield* Effect.log(\"Server started on port 3456. Press Ctrl+C to exit.\");\n\n // For testing purposes, we'll run for a short time instead of forever\n yield* Effect.sleep(\"2 seconds\");\n yield* Effect.log(\"Shutting down gracefully...\");\n});\n\n// 3. Provide the layer and launch with runFork\nconst app = Effect.provide(server.pipe(Effect.scoped), Database.Default);\n\n// 4. Run the app and handle shutdown\nEffect.runPromise(app).catch((error) => {\n Effect.runSync(Effect.logError(\"Application error: \" + error));\n process.exit(1);\n});\n```\n\n---", - "antiPattern": "Letting the Node.js process exit without proper cleanup. If you run a long-running effect with `Effect.runPromise` or don't handle OS signals, pressing Ctrl+C will terminate the process abruptly, and none of your `Effect` finalizers will have a chance to run.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { app } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This will run the server, but Ctrl+C will kill it instantly.\n// The database connection finalizer will NOT be called.\nEffect.runPromise(app);\n```", - "explanation": "When a server process is terminated, you need to ensure that it cleans up properly. This includes closing database connections, finishing in-flight requests, and releasing file handles. Failing to do so can lead to resource leaks or data corruption.\n\nEffect's structured concurrency makes this robust and easy. When a fiber is interrupted, Effect guarantees that it will run all finalizers registered within that fiber's scope, in the reverse order they were acquired.\n\nBy launching your app with `runFork`, you get a `Fiber` that represents the entire application. Triggering `Fiber.interrupt` on this top-level fiber initiates a clean, orderly shutdown sequence for all its resources.\n\n---", - "content": "## Guideline\n\nTo enable graceful shutdown for a long-running application:\n\n1. Define services with cleanup logic in `scoped` `Layer`s using `Effect.addFinalizer` or `Effect.acquireRelease`.\n2. Launch your main application `Effect` using `Effect.runFork` to get a `Fiber` handle to the running process.\n3. Set up listeners for process signals like `SIGINT` (Ctrl+C) and `SIGTERM`.\n4. In the signal handler, call `Fiber.interrupt` on your application's fiber.\n\n---\n\n## Rationale\n\nWhen a server process is terminated, you need to ensure that it cleans up properly. This includes closing database connections, finishing in-flight requests, and releasing file handles. Failing to do so can lead to resource leaks or data corruption.\n\nEffect's structured concurrency makes this robust and easy. When a fiber is interrupted, Effect guarantees that it will run all finalizers registered within that fiber's scope, in the reverse order they were acquired.\n\nBy launching your app with `runFork`, you get a `Fiber` that represents the entire application. Triggering `Fiber.interrupt` on this top-level fiber initiates a clean, orderly shutdown sequence for all its resources.\n\n---\n\n## Good Example\n\nThis example creates a server with a \"scoped\" database connection. It uses `runFork` to start the server and sets up a `SIGINT` handler to interrupt the server fiber, which in turn guarantees the database finalizer is called.\n\n```typescript\nimport { Effect, Layer, Fiber, Context, Scope } from \"effect\";\nimport * as http from \"http\";\n\n// 1. A service with a finalizer for cleanup\nclass Database extends Effect.Service()(\"Database\", {\n effect: Effect.gen(function* () {\n yield* Effect.log(\"Acquiring DB connection\");\n return {\n query: () => Effect.succeed(\"data\"),\n };\n }),\n}) {}\n\n// 2. The main server logic\nconst server = Effect.gen(function* () {\n const db = yield* Database;\n\n // Create server with proper error handling\n const httpServer = yield* Effect.sync(() => {\n const server = http.createServer((_req, res) => {\n Effect.runFork(\n Effect.provide(\n db.query().pipe(Effect.map((data) => res.end(data))),\n Database.Default\n )\n );\n });\n return server;\n });\n\n // Add a finalizer to close the server\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n httpServer.close();\n yield* Effect.log(\"Server closed\");\n })\n );\n\n // Start server with error handling\n yield* Effect.async((resume) => {\n httpServer.once(\"error\", (err: Error) => {\n resume(Effect.fail(new Error(`Failed to start server: ${err.message}`)));\n });\n\n httpServer.listen(3456, () => {\n resume(Effect.succeed(void 0));\n });\n });\n\n yield* Effect.log(\"Server started on port 3456. Press Ctrl+C to exit.\");\n\n // For testing purposes, we'll run for a short time instead of forever\n yield* Effect.sleep(\"2 seconds\");\n yield* Effect.log(\"Shutting down gracefully...\");\n});\n\n// 3. Provide the layer and launch with runFork\nconst app = Effect.provide(server.pipe(Effect.scoped), Database.Default);\n\n// 4. Run the app and handle shutdown\nEffect.runPromise(app).catch((error) => {\n Effect.runSync(Effect.logError(\"Application error: \" + error));\n process.exit(1);\n});\n```\n\n---\n\n## Anti-Pattern\n\nLetting the Node.js process exit without proper cleanup. If you run a long-running effect with `Effect.runPromise` or don't handle OS signals, pressing Ctrl+C will terminate the process abruptly, and none of your `Effect` finalizers will have a chance to run.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { app } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This will run the server, but Ctrl+C will kill it instantly.\n// The database connection finalizer will NOT be called.\nEffect.runPromise(app);\n```" - }, - { - "id": "observability-effect-fn", - "title": "Instrument and Observe Function Calls with Effect.fn", - "description": "Use Effect.fn to wrap functions with effectful instrumentation, such as logging, metrics, or tracing, in a composable and type-safe way.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// A simple function to instrument\nfunction add(a: number, b: number): number {\n return a + b;\n}\n\n// Use Effect.fn to instrument the function with observability\nconst addWithLogging = Effect.fn(\"add\")(add).pipe(\n Effect.withSpan(\"add\", { attributes: { \"fn.name\": \"add\" } })\n);\n\n// Use the instrumented function in an Effect workflow\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Calling add function\");\n const sum = yield* addWithLogging(2, 3);\n yield* Effect.logInfo(`Sum is ${sum}`);\n return sum;\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:**\n\n- `Effect.fn(\"name\")(fn)` wraps a function with instrumentation capabilities, enabling observability.\n- You can add tracing spans, logging, metrics, and other observability logic to function boundaries.\n- Keeps instrumentation separate from business logic and fully composable.\n- The wrapped function integrates seamlessly with Effect's observability and tracing infrastructure.", - "antiPattern": "Scattering logging, metrics, or tracing logic directly inside business functions, making code harder to test, maintain, and compose.", - "explanation": "Instrumenting function calls is essential for observability, especially in complex or critical code paths. \n`Effect.fn` lets you add effectful logic (logging, metrics, tracing, etc.) before, after, or around any function call, without changing the function’s core logic.", - "content": "# Instrument and Observe Function Calls with `Effect.fn`\n\n## Guideline\n\nUse `Effect.fn` to wrap and instrument function calls with effectful logic, such as logging, metrics, or tracing. \nThis enables you to observe, monitor, and debug function boundaries in a composable, type-safe way.\n\n## Rationale\n\nInstrumenting function calls is essential for observability, especially in complex or critical code paths. \n`Effect.fn` lets you add effectful logic (logging, metrics, tracing, etc.) before, after, or around any function call, without changing the function’s core logic.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// A simple function to instrument\nfunction add(a: number, b: number): number {\n return a + b;\n}\n\n// Use Effect.fn to instrument the function with observability\nconst addWithLogging = Effect.fn(\"add\")(add).pipe(\n Effect.withSpan(\"add\", { attributes: { \"fn.name\": \"add\" } })\n);\n\n// Use the instrumented function in an Effect workflow\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Calling add function\");\n const sum = yield* addWithLogging(2, 3);\n yield* Effect.logInfo(`Sum is ${sum}`);\n return sum;\n});\n\n// Run the program\nEffect.runPromise(program);\n```\n\n**Explanation:**\n\n- `Effect.fn(\"name\")(fn)` wraps a function with instrumentation capabilities, enabling observability.\n- You can add tracing spans, logging, metrics, and other observability logic to function boundaries.\n- Keeps instrumentation separate from business logic and fully composable.\n- The wrapped function integrates seamlessly with Effect's observability and tracing infrastructure.\n\n## Anti-Pattern\n\nScattering logging, metrics, or tracing logic directly inside business functions, making code harder to test, maintain, and compose." - }, - { - "id": "observability-opentelemetry", - "title": "Integrate Effect Tracing with OpenTelemetry", - "description": "Integrate Effect.withSpan with OpenTelemetry to export traces and visualize request flows across services.", - "skillLevel": "advanced", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n// Pseudocode: Replace with actual OpenTelemetry integration for your stack\nimport { trace, context, SpanStatusCode } from \"@opentelemetry/api\";\n\n// Wrap an Effect.withSpan to export to OpenTelemetry\nfunction withOtelSpan(\n name: string,\n effect: Effect.Effect\n) {\n return Effect.gen(function* () {\n const otelSpan = trace.getTracer(\"default\").startSpan(name);\n try {\n const result = yield* effect;\n otelSpan.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });\n throw err;\n } finally {\n otelSpan.end();\n }\n });\n}\n\n// Usage\nconst program = withOtelSpan(\n \"fetchUser\",\n Effect.sync(() => {\n // ...fetch user logic\n return { id: 1, name: \"Alice\" };\n })\n);\n```\n\n**Explanation:**\n\n- Start an OpenTelemetry span when entering an Effectful operation.\n- Set status and attributes as needed.\n- End the span when the operation completes or fails.\n- This enables full distributed tracing and visualization in your observability platform.", - "antiPattern": "Using Effect.withSpan without exporting to OpenTelemetry, or lacking distributed tracing, which limits your ability to diagnose and visualize complex request flows.", - "explanation": "OpenTelemetry is the industry standard for distributed tracing. \nBy integrating Effect's spans with OpenTelemetry, you gain deep visibility into request flows, performance bottlenecks, and dependencies—across all your services and infrastructure.", - "content": "# Integrate Effect Tracing with OpenTelemetry\n\n## Guideline\n\nConnect Effect's tracing spans to OpenTelemetry to enable distributed tracing, visualization, and correlation across your entire stack.\n\n## Rationale\n\nOpenTelemetry is the industry standard for distributed tracing. \nBy integrating Effect's spans with OpenTelemetry, you gain deep visibility into request flows, performance bottlenecks, and dependencies—across all your services and infrastructure.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n// Pseudocode: Replace with actual OpenTelemetry integration for your stack\nimport { trace, context, SpanStatusCode } from \"@opentelemetry/api\";\n\n// Wrap an Effect.withSpan to export to OpenTelemetry\nfunction withOtelSpan(\n name: string,\n effect: Effect.Effect\n) {\n return Effect.gen(function* () {\n const otelSpan = trace.getTracer(\"default\").startSpan(name);\n try {\n const result = yield* effect;\n otelSpan.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });\n throw err;\n } finally {\n otelSpan.end();\n }\n });\n}\n\n// Usage\nconst program = withOtelSpan(\n \"fetchUser\",\n Effect.sync(() => {\n // ...fetch user logic\n return { id: 1, name: \"Alice\" };\n })\n);\n```\n\n**Explanation:**\n\n- Start an OpenTelemetry span when entering an Effectful operation.\n- Set status and attributes as needed.\n- End the span when the operation completes or fails.\n- This enables full distributed tracing and visualization in your observability platform.\n\n## Anti-Pattern\n\nUsing Effect.withSpan without exporting to OpenTelemetry, or lacking distributed tracing, which limits your ability to diagnose and visualize complex request flows." - }, - { - "id": "observability-structured-logging", - "title": "Leverage Effect's Built-in Structured Logging", - "description": "Use Effect.log, Effect.logInfo, and Effect.logError to add structured, context-aware logging to your Effect code.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Log a simple message\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting the application\");\n});\n\n// Log at different levels\nconst infoProgram = Effect.gen(function* () {\n yield* Effect.logInfo(\"User signed in\");\n});\n\nconst errorProgram = Effect.gen(function* () {\n yield* Effect.logError(\"Failed to connect to database\");\n});\n\n// Log with dynamic values\nconst userId = 42;\nconst logUserProgram = Effect.gen(function* () {\n yield* Effect.logInfo(`Processing user: ${userId}`);\n});\n\n// Use logging in a workflow\nconst workflow = Effect.gen(function* () {\n yield* Effect.log(\"Beginning workflow\");\n // ... do some work\n yield* Effect.logInfo(\"Workflow step completed\");\n // ... handle errors\n yield* Effect.logError(\"Something went wrong\");\n});\n```\n\n**Explanation:**\n\n- `Effect.log` logs a message at the default level.\n- `Effect.logInfo` and `Effect.logError` log at specific levels.\n- Logging is context-aware and can be used anywhere in your Effect workflows.", - "antiPattern": "Using `console.log` or ad-hoc logging scattered throughout your code, which is not structured, not context-aware, and harder to manage in production.", - "explanation": "Structured logging makes it easier to search, filter, and analyze logs in production. \nEffect’s logging functions are context-aware, meaning they automatically include relevant metadata and can be configured globally.", - "content": "# Leverage Effect's Built-in Structured Logging\n\n## Guideline\n\nUse `Effect.log`, `Effect.logInfo`, `Effect.logError`, and related functions to add structured, context-aware logging to your Effect code. \nThis enables you to capture important events, errors, and business information in a consistent and configurable way.\n\n## Rationale\n\nStructured logging makes it easier to search, filter, and analyze logs in production. \nEffect’s logging functions are context-aware, meaning they automatically include relevant metadata and can be configured globally.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Log a simple message\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting the application\");\n});\n\n// Log at different levels\nconst infoProgram = Effect.gen(function* () {\n yield* Effect.logInfo(\"User signed in\");\n});\n\nconst errorProgram = Effect.gen(function* () {\n yield* Effect.logError(\"Failed to connect to database\");\n});\n\n// Log with dynamic values\nconst userId = 42;\nconst logUserProgram = Effect.gen(function* () {\n yield* Effect.logInfo(`Processing user: ${userId}`);\n});\n\n// Use logging in a workflow\nconst workflow = Effect.gen(function* () {\n yield* Effect.log(\"Beginning workflow\");\n // ... do some work\n yield* Effect.logInfo(\"Workflow step completed\");\n // ... handle errors\n yield* Effect.logError(\"Something went wrong\");\n});\n```\n\n**Explanation:**\n\n- `Effect.log` logs a message at the default level.\n- `Effect.logInfo` and `Effect.logError` log at specific levels.\n- Logging is context-aware and can be used anywhere in your Effect workflows.\n\n## Anti-Pattern\n\nUsing `console.log` or ad-hoc logging scattered throughout your code, which is not structured, not context-aware, and harder to manage in production." - }, - { - "id": "leverage-structured-logging", - "title": "Leverage Effect's Built-in Structured Logging", - "description": "Leverage Effect's built-in structured logging.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.logDebug(\"Processing user\", { userId: 123 });\n\n// Run the program with debug logging enabled\nEffect.runSync(\n program.pipe(Effect.tap(() => Effect.log(\"Debug logging enabled\")))\n);\n```\n\n**Explanation:** \nUsing Effect's logging system ensures your logs are structured, filterable,\nand context-aware.", - "antiPattern": "Calling `console.log` directly within an Effect composition. This is an\nunmanaged side-effect that bypasses all the benefits of Effect's logging system.", - "explanation": "Effect's logger is structured, context-aware (with trace IDs), configurable\nvia `Layer`, and testable. It's a first-class citizen, not an unmanaged\nside-effect.", - "content": "# Leverage Effect's Built-in Structured Logging\n\n## Guideline\n\nUse the built-in `Effect.log*` family of functions for all application logging\ninstead of using `console.log`.\n\n## Rationale\n\nEffect's logger is structured, context-aware (with trace IDs), configurable\nvia `Layer`, and testable. It's a first-class citizen, not an unmanaged\nside-effect.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.logDebug(\"Processing user\", { userId: 123 });\n\n// Run the program with debug logging enabled\nEffect.runSync(\n program.pipe(Effect.tap(() => Effect.log(\"Debug logging enabled\")))\n);\n```\n\n**Explanation:** \nUsing Effect's logging system ensures your logs are structured, filterable,\nand context-aware.\n\n## Anti-Pattern\n\nCalling `console.log` directly within an Effect composition. This is an\nunmanaged side-effect that bypasses all the benefits of Effect's logging system." - }, - { - "id": "constructor-fail-none-left", - "title": "Lifting Errors and Absence with fail, none, and left", - "description": "Use fail, none, and left to create Effect, Option, or Either that represent failure or absence.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Represent a failure with an error value\nconst effect = Effect.fail(\"Something went wrong\"); // Effect\n\n// Option: Represent absence of a value\nconst option = Option.none(); // Option\n\n// Either: Represent a failure with a left value\nconst either = Either.left(\"Invalid input\"); // Either\n```\n\n**Explanation:**\n\n- `Effect.fail(error)` creates an effect that always fails with `error`.\n- `Option.none()` creates an option that is always absent.\n- `Either.left(error)` creates an either that always represents failure.", - "antiPattern": "Throwing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Either world. \nThis makes error handling ad hoc, less type-safe, and harder to compose.", - "explanation": "By lifting errors and absence into these structures, you can handle them declaratively with combinators, rather than relying on exceptions, `null`, or `undefined`. \nThis leads to more robust and maintainable code.", - "content": "# Lifting Errors and Absence with `fail`, `none`, and `left`\n\n## Guideline\n\nUse the `fail`, `none`, and `left` constructors to represent errors or absence in the Effect, Option, or Either world. \nThis makes failures explicit, type-safe, and composable.\n\n## Rationale\n\nBy lifting errors and absence into these structures, you can handle them declaratively with combinators, rather than relying on exceptions, `null`, or `undefined`. \nThis leads to more robust and maintainable code.\n\n## Good Example\n\n```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Represent a failure with an error value\nconst effect = Effect.fail(\"Something went wrong\"); // Effect\n\n// Option: Represent absence of a value\nconst option = Option.none(); // Option\n\n// Either: Represent a failure with a left value\nconst either = Either.left(\"Invalid input\"); // Either\n```\n\n**Explanation:**\n\n- `Effect.fail(error)` creates an effect that always fails with `error`.\n- `Option.none()` creates an option that is always absent.\n- `Either.left(error)` creates an either that always represents failure.\n\n## Anti-Pattern\n\nThrowing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Either world. \nThis makes error handling ad hoc, less type-safe, and harder to compose." - }, - { - "id": "constructor-succeed-some-right", - "title": "Lifting Values with succeed, some, and right", - "description": "Use succeed, some, and right to create Effect, Option, or Either from plain values.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Lift a value into an Effect that always succeeds\nconst effect = Effect.succeed(42); // Effect\n\n// Option: Lift a value into an Option that is always Some\nconst option = Option.some(\"hello\"); // Option\n\n// Either: Lift a value into an Either that is always Right\nconst either = Either.right({ id: 1 }); // Either\n```\n\n**Explanation:**\n\n- `Effect.succeed(value)` creates an effect that always succeeds with `value`.\n- `Option.some(value)` creates an option that is always present.\n- `Either.right(value)` creates an either that always represents success.", - "antiPattern": "Passing plain values around outside the Effect, Option, or Either world, or using `null`/`undefined` to represent absence or success. \nThis leads to less composable, less type-safe code and makes error handling harder.", - "explanation": "Lifting values into these structures allows you to compose them with other effects, options, or eithers, and to take advantage of all the combinators and error handling that Effect provides.", - "content": "# Lifting Values with `succeed`, `some`, and `right`\n\n## Guideline\n\nUse the `succeed`, `some`, and `right` constructors to lift plain values into the Effect, Option, or Either world. \nThis is the foundation for building composable, type-safe programs.\n\n## Rationale\n\nLifting values into these structures allows you to compose them with other effects, options, or eithers, and to take advantage of all the combinators and error handling that Effect provides.\n\n## Good Example\n\n```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Lift a value into an Effect that always succeeds\nconst effect = Effect.succeed(42); // Effect\n\n// Option: Lift a value into an Option that is always Some\nconst option = Option.some(\"hello\"); // Option\n\n// Either: Lift a value into an Either that is always Right\nconst either = Either.right({ id: 1 }); // Either\n```\n\n**Explanation:**\n\n- `Effect.succeed(value)` creates an effect that always succeeds with `value`.\n- `Option.some(value)` creates an option that is always present.\n- `Either.right(value)` creates an either that always represents success.\n\n## Anti-Pattern\n\nPassing plain values around outside the Effect, Option, or Either world, or using `null`/`undefined` to represent absence or success. \nThis leads to less composable, less type-safe code and makes error handling harder." - }, - { - "id": "http-logging", - "title": "Log HTTP Requests and Responses", - "description": "Use Effect's logging to trace HTTP requests for debugging and monitoring.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Duration } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Simple request/response logging\n// ============================================\n\nconst withLogging = (\n request: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now()\n yield* Effect.log(\"→ HTTP Request starting...\")\n\n const result = yield* request\n\n const duration = Date.now() - startTime\n yield* Effect.log(`← HTTP Response received (${duration}ms)`)\n\n return result\n })\n\n// ============================================\n// 2. Detailed request logging\n// ============================================\n\ninterface RequestLog {\n method: string\n url: string\n headers: Record\n body?: unknown\n}\n\ninterface ResponseLog {\n status: number\n headers: Record\n duration: number\n size?: number\n}\n\nconst makeLoggingClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n const logRequest = (method: string, url: string, headers: Record) =>\n Effect.log(\"HTTP Request\").pipe(\n Effect.annotateLogs({\n method,\n url,\n headers: JSON.stringify(headers),\n })\n )\n\n const logResponse = (status: number, duration: number, headers: Record) =>\n Effect.log(\"HTTP Response\").pipe(\n Effect.annotateLogs({\n status: String(status),\n duration: `${duration}ms`,\n headers: JSON.stringify(headers),\n })\n )\n\n return {\n get: (url: string, options?: { headers?: Record }) =>\n Effect.gen(function* () {\n const headers = options?.headers ?? {}\n yield* logRequest(\"GET\", url, headers)\n const startTime = Date.now()\n\n const response = yield* baseClient.get(url)\n\n yield* logResponse(\n response.status,\n Date.now() - startTime,\n response.headers\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n\n post: (url: string, body: unknown, options?: { headers?: Record }) =>\n Effect.gen(function* () {\n const headers = options?.headers ?? {}\n yield* logRequest(\"POST\", url, headers).pipe(\n Effect.annotateLogs(\"body\", JSON.stringify(body).slice(0, 200))\n )\n const startTime = Date.now()\n\n const request = yield* HttpClientRequest.post(url).pipe(\n HttpClientRequest.jsonBody(body)\n )\n const response = yield* baseClient.execute(request)\n\n yield* logResponse(\n response.status,\n Date.now() - startTime,\n response.headers\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n }\n})\n\n// ============================================\n// 3. Log with span for timing\n// ============================================\n\nconst fetchWithSpan = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.withLogSpan(`HTTP GET ${url}`)\n )\n })\n\n// ============================================\n// 4. Conditional logging (debug mode)\n// ============================================\n\nconst makeConditionalLoggingClient = (debug: boolean) =>\n Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n const maybeLog = (message: string, data?: Record) =>\n debug\n ? Effect.log(message).pipe(\n data ? Effect.annotateLogs(data) : (e) => e\n )\n : Effect.void\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n yield* maybeLog(\"HTTP Request\", { method: \"GET\", url })\n const startTime = Date.now()\n\n const response = yield* baseClient.get(url)\n\n yield* maybeLog(\"HTTP Response\", {\n status: String(response.status),\n duration: `${Date.now() - startTime}ms`,\n })\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n }\n })\n\n// ============================================\n// 5. Request ID tracking\n// ============================================\n\nconst makeTrackedClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n const requestId = crypto.randomUUID().slice(0, 8)\n\n yield* Effect.log(\"HTTP Request\").pipe(\n Effect.annotateLogs({\n requestId,\n method: \"GET\",\n url,\n })\n )\n\n const startTime = Date.now()\n const response = yield* baseClient.get(url)\n\n yield* Effect.log(\"HTTP Response\").pipe(\n Effect.annotateLogs({\n requestId,\n status: String(response.status),\n duration: `${Date.now() - startTime}ms`,\n })\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n })\n }\n})\n\n// ============================================\n// 6. Error logging\n// ============================================\n\nconst fetchWithErrorLogging = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status >= 400) {\n return Effect.gen(function* () {\n yield* Effect.logError(\"HTTP Error\").pipe(\n Effect.annotateLogs({\n url,\n status: String(response.status),\n })\n )\n return yield* Effect.fail(new Error(`HTTP ${response.status}`))\n })\n }\n return Effect.succeed(response)\n }),\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.tapError((error) =>\n Effect.logError(\"Request failed\").pipe(\n Effect.annotateLogs({\n url,\n error: String(error),\n })\n )\n )\n )\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeLoggingClient\n\n yield* Effect.log(\"Starting HTTP operations...\")\n\n const data = yield* client.get(\"https://api.example.com/users\")\n\n yield* Effect.log(\"Operations complete\")\n})\n```", - "antiPattern": "", - "explanation": "HTTP logging helps with:\n\n1. **Debugging** - See what's being sent/received\n2. **Performance** - Track slow requests\n3. **Auditing** - Record API usage\n4. **Troubleshooting** - Diagnose production issues\n\n---", - "content": "## Guideline\n\nWrap HTTP clients with logging middleware to capture request details, response info, and timing for debugging and observability.\n\n---\n\n## Rationale\n\nHTTP logging helps with:\n\n1. **Debugging** - See what's being sent/received\n2. **Performance** - Track slow requests\n3. **Auditing** - Record API usage\n4. **Troubleshooting** - Diagnose production issues\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Duration } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\n\n// ============================================\n// 1. Simple request/response logging\n// ============================================\n\nconst withLogging = (\n request: Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now()\n yield* Effect.log(\"→ HTTP Request starting...\")\n\n const result = yield* request\n\n const duration = Date.now() - startTime\n yield* Effect.log(`← HTTP Response received (${duration}ms)`)\n\n return result\n })\n\n// ============================================\n// 2. Detailed request logging\n// ============================================\n\ninterface RequestLog {\n method: string\n url: string\n headers: Record\n body?: unknown\n}\n\ninterface ResponseLog {\n status: number\n headers: Record\n duration: number\n size?: number\n}\n\nconst makeLoggingClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n const logRequest = (method: string, url: string, headers: Record) =>\n Effect.log(\"HTTP Request\").pipe(\n Effect.annotateLogs({\n method,\n url,\n headers: JSON.stringify(headers),\n })\n )\n\n const logResponse = (status: number, duration: number, headers: Record) =>\n Effect.log(\"HTTP Response\").pipe(\n Effect.annotateLogs({\n status: String(status),\n duration: `${duration}ms`,\n headers: JSON.stringify(headers),\n })\n )\n\n return {\n get: (url: string, options?: { headers?: Record }) =>\n Effect.gen(function* () {\n const headers = options?.headers ?? {}\n yield* logRequest(\"GET\", url, headers)\n const startTime = Date.now()\n\n const response = yield* baseClient.get(url)\n\n yield* logResponse(\n response.status,\n Date.now() - startTime,\n response.headers\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n\n post: (url: string, body: unknown, options?: { headers?: Record }) =>\n Effect.gen(function* () {\n const headers = options?.headers ?? {}\n yield* logRequest(\"POST\", url, headers).pipe(\n Effect.annotateLogs(\"body\", JSON.stringify(body).slice(0, 200))\n )\n const startTime = Date.now()\n\n const request = yield* HttpClientRequest.post(url).pipe(\n HttpClientRequest.jsonBody(body)\n )\n const response = yield* baseClient.execute(request)\n\n yield* logResponse(\n response.status,\n Date.now() - startTime,\n response.headers\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n }\n})\n\n// ============================================\n// 3. Log with span for timing\n// ============================================\n\nconst fetchWithSpan = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.withLogSpan(`HTTP GET ${url}`)\n )\n })\n\n// ============================================\n// 4. Conditional logging (debug mode)\n// ============================================\n\nconst makeConditionalLoggingClient = (debug: boolean) =>\n Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n const maybeLog = (message: string, data?: Record) =>\n debug\n ? Effect.log(message).pipe(\n data ? Effect.annotateLogs(data) : (e) => e\n )\n : Effect.void\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n yield* maybeLog(\"HTTP Request\", { method: \"GET\", url })\n const startTime = Date.now()\n\n const response = yield* baseClient.get(url)\n\n yield* maybeLog(\"HTTP Response\", {\n status: String(response.status),\n duration: `${Date.now() - startTime}ms`,\n })\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n }),\n }\n })\n\n// ============================================\n// 5. Request ID tracking\n// ============================================\n\nconst makeTrackedClient = Effect.gen(function* () {\n const baseClient = yield* HttpClient.HttpClient\n\n return {\n get: (url: string) =>\n Effect.gen(function* () {\n const requestId = crypto.randomUUID().slice(0, 8)\n\n yield* Effect.log(\"HTTP Request\").pipe(\n Effect.annotateLogs({\n requestId,\n method: \"GET\",\n url,\n })\n )\n\n const startTime = Date.now()\n const response = yield* baseClient.get(url)\n\n yield* Effect.log(\"HTTP Response\").pipe(\n Effect.annotateLogs({\n requestId,\n status: String(response.status),\n duration: `${Date.now() - startTime}ms`,\n })\n )\n\n return yield* HttpClientResponse.json(response) as Effect.Effect\n })\n }\n})\n\n// ============================================\n// 6. Error logging\n// ============================================\n\nconst fetchWithErrorLogging = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status >= 400) {\n return Effect.gen(function* () {\n yield* Effect.logError(\"HTTP Error\").pipe(\n Effect.annotateLogs({\n url,\n status: String(response.status),\n })\n )\n return yield* Effect.fail(new Error(`HTTP ${response.status}`))\n })\n }\n return Effect.succeed(response)\n }),\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.tapError((error) =>\n Effect.logError(\"Request failed\").pipe(\n Effect.annotateLogs({\n url,\n error: String(error),\n })\n )\n )\n )\n })\n\n// ============================================\n// 7. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n const client = yield* makeLoggingClient\n\n yield* Effect.log(\"Starting HTTP operations...\")\n\n const data = yield* client.get(\"https://api.example.com/users\")\n\n yield* Effect.log(\"Operations complete\")\n})\n```\n\n## Log Levels\n\n| Level | Use For |\n|-------|---------|\n| Debug | Full request/response bodies |\n| Info | Request method, URL, status |\n| Warning | Slow requests, retries |\n| Error | Failed requests, 5xx responses |\n\n## What to Log\n\n| Request | Response |\n|---------|----------|\n| Method, URL | Status code |\n| Headers (sanitized) | Headers |\n| Body (truncated) | Duration |\n| Timestamp | Size |\n\n## Best Practices\n\n1. **Redact sensitive data** - Don't log auth tokens, passwords\n2. **Truncate bodies** - Limit logged size\n3. **Use structured logs** - JSON for parsing\n4. **Include request IDs** - Correlate logs\n5. **Log errors separately** - Different levels" - }, - { - "id": "make-http-client-request", - "title": "Make an Outgoing HTTP Client Request", - "description": "Use the Http.client module to make outgoing requests to keep the entire operation within the Effect ecosystem.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "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.\n\n```typescript\nimport { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\nimport * as HttpRouter from \"@effect/platform/HttpRouter\";\nimport * as HttpServer from \"@effect/platform/HttpServer\";\nimport * as HttpResponse from \"@effect/platform/HttpServerResponse\";\nimport { Console, Data, Duration, Effect, Fiber, Layer } from \"effect\";\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: string) =>\n id === \"123\"\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError({ id })),\n }),\n}) {}\n\nconst userHandler = Effect.flatMap(HttpRouter.params, (p) =>\n Effect.flatMap(Database, (db) => db.getUser(p[\"userId\"] ?? \"\")).pipe(\n Effect.flatMap(HttpResponse.json)\n )\n);\n\nconst app = HttpRouter.empty.pipe(\n HttpRouter.get(\"/users/:userId\", userHandler)\n);\n\nconst server = NodeHttpServer.layer(() => require(\"node:http\").createServer(), {\n port: 3457,\n});\n\nconst serverLayer = HttpServer.serve(app);\n\nconst mainLayer = Layer.merge(Database.Default, server);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Server started on http://localhost:3457\");\n const layer = Layer.provide(serverLayer, mainLayer);\n\n // Launch server and run for a short duration to demonstrate\n const serverFiber = yield* Layer.launch(layer).pipe(Effect.fork);\n\n // Wait a moment for server to start\n yield* Effect.sleep(Duration.seconds(1));\n\n // Simulate some server activity\n yield* Effect.log(\"Server is running and ready to handle requests\");\n yield* Effect.sleep(Duration.seconds(2));\n\n // Shutdown gracefully\n yield* Fiber.interrupt(serverFiber);\n yield* Effect.log(\"Server shutdown complete\");\n});\n\nNodeRuntime.runMain(\n Effect.provide(\n program,\n Layer.provide(serverLayer, Layer.merge(Database.Default, server))\n ) as Effect.Effect\n);\n```", - "antiPattern": "The anti-pattern is to use `fetch` inside a route handler, wrapped in `Effect.tryPromise`. This approach requires manual error handling and loses the benefits of the Effect ecosystem.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst proxyRoute = Http.router.get(\n \"/proxy/posts/:id\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n // Manually wrap fetch in an Effect\n Effect.tryPromise({\n try: () =>\n fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`),\n catch: () => \"FetchError\", // Untyped error\n }).pipe(\n Effect.flatMap((res) =>\n // Manually check status and parse JSON, each with its own error case\n res.ok\n ? Effect.tryPromise({\n try: () => res.json(),\n catch: () => \"JsonError\",\n })\n : Effect.fail(\"BadStatusError\")\n ),\n Effect.map(Http.response.json),\n // A generic catch-all because we can't easily distinguish error types\n Effect.catchAll(() =>\n Http.response.text(\"An unknown error occurred\", { status: 500 })\n )\n )\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(proxyRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual approach is significantly more complex and less safe. It forces you to reinvent status and parsing logic, uses untyped string-based errors, and most importantly, the `fetch` call will not be automatically interrupted if the parent request is cancelled.", - "explanation": "An API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `Http.client` is superior for several critical reasons:\n\n1. **Full Integration**: An `Http.client` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know.\n2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `Http.client` request. A raw `fetch` call would continue running in the background, wasting resources.\n3. **Typed Errors**: The client provides a rich set of typed errors (e.g., `Http.error.RequestError`, `Http.error.ResponseError`). This allows you to write precise error handling logic to distinguish between a network failure and a non-2xx response from the external API.\n4. **Testability**: The `Http.client` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests.\n\n---", - "content": "## Guideline\n\nTo call an external API from within your server, use the `Http.client` module. This creates an `Effect` that represents the outgoing request, keeping it fully integrated with the Effect runtime.\n\n---\n\n## Rationale\n\nAn API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `Http.client` is superior for several critical reasons:\n\n1. **Full Integration**: An `Http.client` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know.\n2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `Http.client` request. A raw `fetch` call would continue running in the background, wasting resources.\n3. **Typed Errors**: The client provides a rich set of typed errors (e.g., `Http.error.RequestError`, `Http.error.ResponseError`). This allows you to write precise error handling logic to distinguish between a network failure and a non-2xx response from the external API.\n4. **Testability**: The `Http.client` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\nimport * as HttpRouter from \"@effect/platform/HttpRouter\";\nimport * as HttpServer from \"@effect/platform/HttpServer\";\nimport * as HttpResponse from \"@effect/platform/HttpServerResponse\";\nimport { Console, Data, Duration, Effect, Fiber, Layer } from \"effect\";\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: string) =>\n id === \"123\"\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError({ id })),\n }),\n}) {}\n\nconst userHandler = Effect.flatMap(HttpRouter.params, (p) =>\n Effect.flatMap(Database, (db) => db.getUser(p[\"userId\"] ?? \"\")).pipe(\n Effect.flatMap(HttpResponse.json)\n )\n);\n\nconst app = HttpRouter.empty.pipe(\n HttpRouter.get(\"/users/:userId\", userHandler)\n);\n\nconst server = NodeHttpServer.layer(() => require(\"node:http\").createServer(), {\n port: 3457,\n});\n\nconst serverLayer = HttpServer.serve(app);\n\nconst mainLayer = Layer.merge(Database.Default, server);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Server started on http://localhost:3457\");\n const layer = Layer.provide(serverLayer, mainLayer);\n\n // Launch server and run for a short duration to demonstrate\n const serverFiber = yield* Layer.launch(layer).pipe(Effect.fork);\n\n // Wait a moment for server to start\n yield* Effect.sleep(Duration.seconds(1));\n\n // Simulate some server activity\n yield* Effect.log(\"Server is running and ready to handle requests\");\n yield* Effect.sleep(Duration.seconds(2));\n\n // Shutdown gracefully\n yield* Fiber.interrupt(serverFiber);\n yield* Effect.log(\"Server shutdown complete\");\n});\n\nNodeRuntime.runMain(\n Effect.provide(\n program,\n Layer.provide(serverLayer, Layer.merge(Database.Default, server))\n ) as Effect.Effect\n);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to use `fetch` inside a route handler, wrapped in `Effect.tryPromise`. This approach requires manual error handling and loses the benefits of the Effect ecosystem.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst proxyRoute = Http.router.get(\n \"/proxy/posts/:id\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n // Manually wrap fetch in an Effect\n Effect.tryPromise({\n try: () =>\n fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`),\n catch: () => \"FetchError\", // Untyped error\n }).pipe(\n Effect.flatMap((res) =>\n // Manually check status and parse JSON, each with its own error case\n res.ok\n ? Effect.tryPromise({\n try: () => res.json(),\n catch: () => \"JsonError\",\n })\n : Effect.fail(\"BadStatusError\")\n ),\n Effect.map(Http.response.json),\n // A generic catch-all because we can't easily distinguish error types\n Effect.catchAll(() =>\n Http.response.text(\"An unknown error occurred\", { status: 500 })\n )\n )\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(proxyRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual approach is significantly more complex and less safe. It forces you to reinvent status and parsing logic, uses untyped string-based errors, and most importantly, the `fetch` call will not be automatically interrupted if the parent request is cancelled." - }, - { - "id": "resource-hierarchies", - "title": "Manage Hierarchical Resources", - "description": "Use nested Scopes to manage resources with parent-child dependencies.", - "skillLevel": "advanced", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Scope, Exit } from \"effect\"\n\n// ============================================\n// 1. Define hierarchical resources\n// ============================================\n\ninterface Database {\n readonly name: string\n readonly createConnection: () => Effect.Effect\n}\n\ninterface Connection {\n readonly id: string\n readonly database: string\n readonly beginTransaction: () => Effect.Effect\n}\n\ninterface Transaction {\n readonly id: string\n readonly connectionId: string\n readonly execute: (sql: string) => Effect.Effect\n}\n\n// ============================================\n// 2. Create resources with proper lifecycle\n// ============================================\n\nconst makeDatabase = (name: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n yield* Effect.log(`Opening database: ${name}`)\n \n const db: Database = {\n name,\n createConnection: () => makeConnection(name),\n }\n \n return db\n }),\n (db) => Effect.log(`Closing database: ${db.name}`)\n )\n\nconst makeConnection = (dbName: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n const id = `conn-${crypto.randomUUID().slice(0, 8)}`\n yield* Effect.log(` Opening connection: ${id} to ${dbName}`)\n \n const conn: Connection = {\n id,\n database: dbName,\n beginTransaction: () => makeTransaction(id),\n }\n \n return conn\n }),\n (conn) => Effect.log(` Closing connection: ${conn.id}`)\n )\n\nconst makeTransaction = (connId: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n const id = `tx-${crypto.randomUUID().slice(0, 8)}`\n yield* Effect.log(` Beginning transaction: ${id}`)\n \n const tx: Transaction = {\n id,\n connectionId: connId,\n execute: (sql) => Effect.log(` [${id}] ${sql}`),\n }\n \n return tx\n }),\n (tx) => Effect.log(` Committing transaction: ${tx.id}`)\n )\n\n// ============================================\n// 3. Use hierarchical resources\n// ============================================\n\nconst program = Effect.scoped(\n Effect.gen(function* () {\n yield* Effect.log(\"=== Starting hierarchical resource demo ===\\n\")\n \n // Level 1: Database\n const db = yield* makeDatabase(\"myapp\")\n \n // Level 2: Connection (child of database)\n const conn = yield* db.createConnection()\n \n // Level 3: Transaction (child of connection)\n const tx = yield* conn.beginTransaction()\n \n // Use the transaction\n yield* tx.execute(\"INSERT INTO users (name) VALUES ('Alice')\")\n yield* tx.execute(\"INSERT INTO users (name) VALUES ('Bob')\")\n \n yield* Effect.log(\"\\n=== Work complete, releasing resources ===\\n\")\n \n // Resources released in reverse order:\n // 1. Transaction committed\n // 2. Connection closed\n // 3. Database closed\n })\n)\n\nEffect.runPromise(program)\n\n// ============================================\n// 4. Multiple children at same level\n// ============================================\n\nconst multipleConnections = Effect.scoped(\n Effect.gen(function* () {\n const db = yield* makeDatabase(\"myapp\")\n \n // Create multiple connections\n const conn1 = yield* db.createConnection()\n const conn2 = yield* db.createConnection()\n \n // Each connection can have transactions\n const tx1 = yield* conn1.beginTransaction()\n const tx2 = yield* conn2.beginTransaction()\n \n // Use both transactions\n yield* Effect.all([\n tx1.execute(\"UPDATE table1 SET x = 1\"),\n tx2.execute(\"UPDATE table2 SET y = 2\"),\n ])\n \n // All released in proper order\n })\n)\n```", - "antiPattern": "", - "explanation": "Resources often have dependencies:\n\n1. **Database → Connections → Transactions** - Transaction needs connection\n2. **Server → Routes → Handlers** - Handler needs server context\n3. **File → Reader → Parser** - Parser needs reader\n\nRelease order matters: children before parents.\n\n---", - "content": "## Guideline\n\nUse nested `Scope` to manage hierarchical resources where child resources depend on their parents and must be released first.\n\n---\n\n## Rationale\n\nResources often have dependencies:\n\n1. **Database → Connections → Transactions** - Transaction needs connection\n2. **Server → Routes → Handlers** - Handler needs server context\n3. **File → Reader → Parser** - Parser needs reader\n\nRelease order matters: children before parents.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Scope, Exit } from \"effect\"\n\n// ============================================\n// 1. Define hierarchical resources\n// ============================================\n\ninterface Database {\n readonly name: string\n readonly createConnection: () => Effect.Effect\n}\n\ninterface Connection {\n readonly id: string\n readonly database: string\n readonly beginTransaction: () => Effect.Effect\n}\n\ninterface Transaction {\n readonly id: string\n readonly connectionId: string\n readonly execute: (sql: string) => Effect.Effect\n}\n\n// ============================================\n// 2. Create resources with proper lifecycle\n// ============================================\n\nconst makeDatabase = (name: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n yield* Effect.log(`Opening database: ${name}`)\n \n const db: Database = {\n name,\n createConnection: () => makeConnection(name),\n }\n \n return db\n }),\n (db) => Effect.log(`Closing database: ${db.name}`)\n )\n\nconst makeConnection = (dbName: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n const id = `conn-${crypto.randomUUID().slice(0, 8)}`\n yield* Effect.log(` Opening connection: ${id} to ${dbName}`)\n \n const conn: Connection = {\n id,\n database: dbName,\n beginTransaction: () => makeTransaction(id),\n }\n \n return conn\n }),\n (conn) => Effect.log(` Closing connection: ${conn.id}`)\n )\n\nconst makeTransaction = (connId: string): Effect.Effect =>\n Effect.acquireRelease(\n Effect.gen(function* () {\n const id = `tx-${crypto.randomUUID().slice(0, 8)}`\n yield* Effect.log(` Beginning transaction: ${id}`)\n \n const tx: Transaction = {\n id,\n connectionId: connId,\n execute: (sql) => Effect.log(` [${id}] ${sql}`),\n }\n \n return tx\n }),\n (tx) => Effect.log(` Committing transaction: ${tx.id}`)\n )\n\n// ============================================\n// 3. Use hierarchical resources\n// ============================================\n\nconst program = Effect.scoped(\n Effect.gen(function* () {\n yield* Effect.log(\"=== Starting hierarchical resource demo ===\\n\")\n \n // Level 1: Database\n const db = yield* makeDatabase(\"myapp\")\n \n // Level 2: Connection (child of database)\n const conn = yield* db.createConnection()\n \n // Level 3: Transaction (child of connection)\n const tx = yield* conn.beginTransaction()\n \n // Use the transaction\n yield* tx.execute(\"INSERT INTO users (name) VALUES ('Alice')\")\n yield* tx.execute(\"INSERT INTO users (name) VALUES ('Bob')\")\n \n yield* Effect.log(\"\\n=== Work complete, releasing resources ===\\n\")\n \n // Resources released in reverse order:\n // 1. Transaction committed\n // 2. Connection closed\n // 3. Database closed\n })\n)\n\nEffect.runPromise(program)\n\n// ============================================\n// 4. Multiple children at same level\n// ============================================\n\nconst multipleConnections = Effect.scoped(\n Effect.gen(function* () {\n const db = yield* makeDatabase(\"myapp\")\n \n // Create multiple connections\n const conn1 = yield* db.createConnection()\n const conn2 = yield* db.createConnection()\n \n // Each connection can have transactions\n const tx1 = yield* conn1.beginTransaction()\n const tx2 = yield* conn2.beginTransaction()\n \n // Use both transactions\n yield* Effect.all([\n tx1.execute(\"UPDATE table1 SET x = 1\"),\n tx2.execute(\"UPDATE table2 SET y = 2\"),\n ])\n \n // All released in proper order\n })\n)\n```\n\n## Output Example\n\n```\n=== Starting hierarchical resource demo ===\n\nOpening database: myapp\n Opening connection: conn-a1b2c3d4 to myapp\n Beginning transaction: tx-e5f6g7h8\n [tx-e5f6g7h8] INSERT INTO users (name) VALUES ('Alice')\n [tx-e5f6g7h8] INSERT INTO users (name) VALUES ('Bob')\n\n=== Work complete, releasing resources ===\n\n Committing transaction: tx-e5f6g7h8\n Closing connection: conn-a1b2c3d4\nClosing database: myapp\n```\n\n## Hierarchy Patterns\n\n| Pattern | Example |\n|---------|---------|\n| **Linear** | DB → Connection → Transaction |\n| **Tree** | Server → [Route1, Route2] |\n| **Diamond** | Pool → [Conn1, Conn2] → SharedCache |\n\n## Best Practices\n\n1. **Use acquireRelease** - Ensures cleanup happens\n2. **Scope nesting** - Child scopes close before parent\n3. **Log lifecycle** - Debug release order issues\n4. **Handle errors** - Cleanup runs even on failure" - }, - { - "id": "manage-resource-lifecycles-with-scope", - "title": "Manage Resource Lifecycles with Scope", - "description": "Use Scope for fine-grained, manual control over resource lifecycles and cleanup guarantees.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This example shows how to acquire a resource (like a file handle), use it, and have `Scope` guarantee its release.\n\n```typescript\nimport { Effect, Scope } from \"effect\";\n\n// Simulate acquiring and releasing a resource\nconst acquireFile = Effect.log(\"File opened\").pipe(\n Effect.as({ write: (data: string) => Effect.log(`Wrote: ${data}`) })\n);\nconst releaseFile = Effect.log(\"File closed.\");\n\n// Create a \"scoped\" effect. This effect, when used, will acquire the\n// resource and register its release action with the current scope.\nconst scopedFile = Effect.acquireRelease(acquireFile, () => releaseFile);\n\n// The main program that uses the scoped resource\nconst program = Effect.gen(function* () {\n // Effect.scoped \"uses\" the resource. It runs the acquire effect,\n // provides the resource to the inner effect, and ensures the\n // release effect is run when this block completes.\n const file = yield* Effect.scoped(scopedFile);\n\n yield* file.write(\"hello\");\n yield* file.write(\"world\");\n\n // The file will be automatically closed here.\n});\n\nEffect.runPromise(program);\n/*\nOutput:\nFile opened\nWrote: hello\nWrote: world\nFile closed\n*/\n```\n\n---", - "antiPattern": "Manual resource management without the guarantees of `Scope`. This is brittle because if an error occurs after the resource is acquired but before it's released, the release logic is never executed.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { acquireFile, releaseFile } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This will leak the resource if an error happens.\nconst program = Effect.gen(function* () {\n const file = yield* acquireFile;\n\n // If this operation fails...\n yield* Effect.fail(\"Something went wrong!\");\n\n // ...this line will never be reached, and the file will never be closed.\n yield* releaseFile;\n});\n```", - "explanation": "`Scope` is the fundamental building block for all resource management in Effect. While higher-level APIs like `Layer.scoped` and `Stream` are often sufficient, understanding `Scope` is key to advanced use cases.\n\nA `Scope` guarantees that any finalizers added to it will be executed when the scope is closed, regardless of whether the associated computation succeeds, fails, or is interrupted. This provides a rock-solid guarantee against resource leaks.\n\nThis is especially critical in concurrent applications. When a parent fiber is interrupted, it closes its scope, which in turn automatically interrupts all its child fibers and runs all their finalizers in a structured, predictable order.\n\n---", - "content": "## Guideline\n\nA `Scope` is a context that collects finalizers (cleanup effects). When you need fine-grained control over resource lifecycles, you can work with `Scope` directly. The most common pattern is to create a resource within a scope using `Effect.acquireRelease` and then use it via `Effect.scoped`.\n\n---\n\n## Rationale\n\n`Scope` is the fundamental building block for all resource management in Effect. While higher-level APIs like `Layer.scoped` and `Stream` are often sufficient, understanding `Scope` is key to advanced use cases.\n\nA `Scope` guarantees that any finalizers added to it will be executed when the scope is closed, regardless of whether the associated computation succeeds, fails, or is interrupted. This provides a rock-solid guarantee against resource leaks.\n\nThis is especially critical in concurrent applications. When a parent fiber is interrupted, it closes its scope, which in turn automatically interrupts all its child fibers and runs all their finalizers in a structured, predictable order.\n\n---\n\n## Good Example\n\nThis example shows how to acquire a resource (like a file handle), use it, and have `Scope` guarantee its release.\n\n```typescript\nimport { Effect, Scope } from \"effect\";\n\n// Simulate acquiring and releasing a resource\nconst acquireFile = Effect.log(\"File opened\").pipe(\n Effect.as({ write: (data: string) => Effect.log(`Wrote: ${data}`) })\n);\nconst releaseFile = Effect.log(\"File closed.\");\n\n// Create a \"scoped\" effect. This effect, when used, will acquire the\n// resource and register its release action with the current scope.\nconst scopedFile = Effect.acquireRelease(acquireFile, () => releaseFile);\n\n// The main program that uses the scoped resource\nconst program = Effect.gen(function* () {\n // Effect.scoped \"uses\" the resource. It runs the acquire effect,\n // provides the resource to the inner effect, and ensures the\n // release effect is run when this block completes.\n const file = yield* Effect.scoped(scopedFile);\n\n yield* file.write(\"hello\");\n yield* file.write(\"world\");\n\n // The file will be automatically closed here.\n});\n\nEffect.runPromise(program);\n/*\nOutput:\nFile opened\nWrote: hello\nWrote: world\nFile closed\n*/\n```\n\n---\n\n## Anti-Pattern\n\nManual resource management without the guarantees of `Scope`. This is brittle because if an error occurs after the resource is acquired but before it's released, the release logic is never executed.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { acquireFile, releaseFile } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This will leak the resource if an error happens.\nconst program = Effect.gen(function* () {\n const file = yield* acquireFile;\n\n // If this operation fails...\n yield* Effect.fail(\"Something went wrong!\");\n\n // ...this line will never be reached, and the file will never be closed.\n yield* releaseFile;\n});\n```" - }, - { - "id": "stream-manage-resources", - "title": "Manage Resources Safely in a Pipeline", - "description": "Use Stream.acquireRelease to safely manage the lifecycle of a resource within a pipeline.", - "skillLevel": "advanced", - "useCases": [ - "building-data-pipelines" - ], - "example": "This example creates and writes to a temporary file. `Stream.acquireRelease` is used to acquire a readable stream from that file. The pipeline then processes the file but is designed to fail partway through. The logs demonstrate that the `release` effect (which deletes the file) is still executed, preventing any resource leaks.\n\n```typescript\nimport { Effect, Layer } from \"effect\";\nimport { FileSystem } from \"@effect/platform/FileSystem\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport * as path from \"node:path\";\n\ninterface ProcessError {\n readonly _tag: \"ProcessError\";\n readonly message: string;\n}\n\nconst ProcessError = (message: string): ProcessError => ({\n _tag: \"ProcessError\",\n message,\n});\n\ninterface FileServiceType {\n readonly createTempFile: () => Effect.Effect<{ filePath: string }, never>;\n readonly cleanup: (filePath: string) => Effect.Effect;\n readonly readFile: (filePath: string) => Effect.Effect;\n}\n\nexport class FileService extends Effect.Service()(\"FileService\", {\n sync: () => {\n const filePath = path.join(__dirname, \"temp-resource.txt\");\n return {\n createTempFile: () => Effect.succeed({ filePath }),\n cleanup: (filePath: string) =>\n Effect.log(\"✅ Resource cleaned up successfully\"),\n readFile: (filePath: string) =>\n Effect.succeed(\"data 1\\ndata 2\\nFAIL\\ndata 4\"),\n };\n },\n}) {}\n\n// Process a single line\nconst processLine = (line: string): Effect.Effect =>\n line === \"FAIL\"\n ? Effect.fail(ProcessError(\"Failed to process line\"))\n : Effect.log(`Processed: ${line}`);\n\n// Create and process the file with proper resource management\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Stream Resource Management Demo ===\");\n yield* Effect.log(\n \"This demonstrates proper resource cleanup even when errors occur\"\n );\n\n const fileService = yield* FileService;\n const { filePath } = yield* fileService.createTempFile();\n\n // Use scoped to ensure cleanup happens even on failure\n yield* Effect.scoped(\n Effect.gen(function* () {\n yield* Effect.addFinalizer(() => fileService.cleanup(filePath));\n\n const content = yield* fileService.readFile(filePath);\n const lines = content.split(\"\\n\");\n\n // Process each line, continuing even if some fail\n for (const line of lines) {\n yield* processLine(line).pipe(\n Effect.catchAll((error) =>\n Effect.log(`⚠️ Skipped line due to error: ${error.message}`)\n )\n );\n }\n\n yield* Effect.log(\n \"✅ Processing completed with proper resource management\"\n );\n })\n );\n});\n\n// Run the program with FileService layer\nEffect.runPromise(Effect.provide(program, FileService.Default)).catch(\n (error) => {\n Effect.runSync(Effect.logError(\"Unexpected error: \" + error));\n }\n);\n```", - "antiPattern": "The anti-pattern is to manage resources manually outside the stream's context. This is brittle and almost always leads to resource leaks when errors occur.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport * as path from \"node:path\";\n\nconst program = Effect.gen(function* () {\n const fs = yield* NodeFileSystem;\n const filePath = path.join(__dirname, \"temp-resource-bad.txt\");\n\n // 1. Resource acquired manually before the stream\n yield* fs.writeFileString(filePath, \"data 1\\ndata 2\");\n const readable = fs.createReadStream(filePath);\n yield* Effect.log(\"Resource acquired manually.\");\n\n const stream = Stream.fromReadable(() => readable).pipe(\n Stream.decodeText(\"utf-8\"),\n Stream.splitLines,\n // This stream will fail, causing the run to reject.\n Stream.map(() => {\n throw new Error(\"Something went wrong!\");\n })\n );\n\n // 2. Stream is executed\n yield* Stream.runDrain(stream);\n\n // 3. This release logic is NEVER reached if the stream fails.\n yield* fs.remove(filePath);\n yield* Effect.log(\"Resource released manually. (This will not be logged)\");\n});\n\nEffect.runPromiseExit(program).then((exit) => {\n if (exit._tag === \"Failure\") {\n console.log(\"\\nPipeline failed. The temp file was NOT deleted.\");\n }\n});\n```\n\nIn this anti-pattern, the `fs.remove` call is unreachable because the `Stream.runDrain` effect fails, causing the `gen` block to terminate immediately. The temporary file is leaked onto the disk. `Stream.acquireRelease` solves this problem entirely.", - "explanation": "What happens if a pipeline processing a file fails halfway through? In a naive implementation, the file handle might be left open, leading to a resource leak. Over time, these leaks can exhaust system resources and crash your application.\n\n`Stream.acquireRelease` is Effect's robust solution to this problem. It's built on `Scope`, Effect's fundamental resource-management tool.\n\n1. **Guaranteed Cleanup**: You provide an `acquire` effect to open the resource and a `release` effect to close it. Effect guarantees that the `release` effect will be called when the stream terminates, for _any_ reason: successful completion, a processing failure, or even external interruption.\n2. **Declarative and Co-located**: The logic for a resource's entire lifecycle—acquisition, usage (the stream itself), and release—is defined in one place. This makes the code easier to understand and reason about compared to manual `try/finally` blocks.\n3. **Prevents Resource Leaks**: It is the idiomatic way to build truly resilient pipelines that do not leak resources, which is essential for long-running, production-grade applications.\n4. **Composability**: The resulting stream is just a normal `Stream`, which can be composed with any other stream operators.\n\n---", - "content": "## Guideline\n\nTo safely manage a resource that has an open/close lifecycle (like a file handle or database connection) for the duration of a stream, use the `Stream.acquireRelease` constructor.\n\n---\n\n## Rationale\n\nWhat happens if a pipeline processing a file fails halfway through? In a naive implementation, the file handle might be left open, leading to a resource leak. Over time, these leaks can exhaust system resources and crash your application.\n\n`Stream.acquireRelease` is Effect's robust solution to this problem. It's built on `Scope`, Effect's fundamental resource-management tool.\n\n1. **Guaranteed Cleanup**: You provide an `acquire` effect to open the resource and a `release` effect to close it. Effect guarantees that the `release` effect will be called when the stream terminates, for _any_ reason: successful completion, a processing failure, or even external interruption.\n2. **Declarative and Co-located**: The logic for a resource's entire lifecycle—acquisition, usage (the stream itself), and release—is defined in one place. This makes the code easier to understand and reason about compared to manual `try/finally` blocks.\n3. **Prevents Resource Leaks**: It is the idiomatic way to build truly resilient pipelines that do not leak resources, which is essential for long-running, production-grade applications.\n4. **Composability**: The resulting stream is just a normal `Stream`, which can be composed with any other stream operators.\n\n---\n\n## Good Example\n\nThis example creates and writes to a temporary file. `Stream.acquireRelease` is used to acquire a readable stream from that file. The pipeline then processes the file but is designed to fail partway through. The logs demonstrate that the `release` effect (which deletes the file) is still executed, preventing any resource leaks.\n\n```typescript\nimport { Effect, Layer } from \"effect\";\nimport { FileSystem } from \"@effect/platform/FileSystem\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport * as path from \"node:path\";\n\ninterface ProcessError {\n readonly _tag: \"ProcessError\";\n readonly message: string;\n}\n\nconst ProcessError = (message: string): ProcessError => ({\n _tag: \"ProcessError\",\n message,\n});\n\ninterface FileServiceType {\n readonly createTempFile: () => Effect.Effect<{ filePath: string }, never>;\n readonly cleanup: (filePath: string) => Effect.Effect;\n readonly readFile: (filePath: string) => Effect.Effect;\n}\n\nexport class FileService extends Effect.Service()(\"FileService\", {\n sync: () => {\n const filePath = path.join(__dirname, \"temp-resource.txt\");\n return {\n createTempFile: () => Effect.succeed({ filePath }),\n cleanup: (filePath: string) =>\n Effect.log(\"✅ Resource cleaned up successfully\"),\n readFile: (filePath: string) =>\n Effect.succeed(\"data 1\\ndata 2\\nFAIL\\ndata 4\"),\n };\n },\n}) {}\n\n// Process a single line\nconst processLine = (line: string): Effect.Effect =>\n line === \"FAIL\"\n ? Effect.fail(ProcessError(\"Failed to process line\"))\n : Effect.log(`Processed: ${line}`);\n\n// Create and process the file with proper resource management\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Stream Resource Management Demo ===\");\n yield* Effect.log(\n \"This demonstrates proper resource cleanup even when errors occur\"\n );\n\n const fileService = yield* FileService;\n const { filePath } = yield* fileService.createTempFile();\n\n // Use scoped to ensure cleanup happens even on failure\n yield* Effect.scoped(\n Effect.gen(function* () {\n yield* Effect.addFinalizer(() => fileService.cleanup(filePath));\n\n const content = yield* fileService.readFile(filePath);\n const lines = content.split(\"\\n\");\n\n // Process each line, continuing even if some fail\n for (const line of lines) {\n yield* processLine(line).pipe(\n Effect.catchAll((error) =>\n Effect.log(`⚠️ Skipped line due to error: ${error.message}`)\n )\n );\n }\n\n yield* Effect.log(\n \"✅ Processing completed with proper resource management\"\n );\n })\n );\n});\n\n// Run the program with FileService layer\nEffect.runPromise(Effect.provide(program, FileService.Default)).catch(\n (error) => {\n Effect.runSync(Effect.logError(\"Unexpected error: \" + error));\n }\n);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to manage resources manually outside the stream's context. This is brittle and almost always leads to resource leaks when errors occur.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport * as path from \"node:path\";\n\nconst program = Effect.gen(function* () {\n const fs = yield* NodeFileSystem;\n const filePath = path.join(__dirname, \"temp-resource-bad.txt\");\n\n // 1. Resource acquired manually before the stream\n yield* fs.writeFileString(filePath, \"data 1\\ndata 2\");\n const readable = fs.createReadStream(filePath);\n yield* Effect.log(\"Resource acquired manually.\");\n\n const stream = Stream.fromReadable(() => readable).pipe(\n Stream.decodeText(\"utf-8\"),\n Stream.splitLines,\n // This stream will fail, causing the run to reject.\n Stream.map(() => {\n throw new Error(\"Something went wrong!\");\n })\n );\n\n // 2. Stream is executed\n yield* Stream.runDrain(stream);\n\n // 3. This release logic is NEVER reached if the stream fails.\n yield* fs.remove(filePath);\n yield* Effect.log(\"Resource released manually. (This will not be logged)\");\n});\n\nEffect.runPromiseExit(program).then((exit) => {\n if (exit._tag === \"Failure\") {\n console.log(\"\\nPipeline failed. The temp file was NOT deleted.\");\n }\n});\n```\n\nIn this anti-pattern, the `fs.remove` call is unreachable because the `Stream.runDrain` effect fails, causing the `gen` block to terminate immediately. The temporary file is leaked onto the disk. `Stream.acquireRelease` solves this problem entirely." - }, - { - "id": "manage-shared-state-with-ref", - "title": "Manage Shared State Safely with Ref", - "description": "Use Ref to manage shared, mutable state concurrently, ensuring atomicity.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "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.\n\n```typescript\nimport { Effect, Ref } from \"effect\";\n\nconst program = Effect.gen(function* () {\n // Create a new Ref with an initial value of 0\n const ref = yield* Ref.make(0);\n\n // Define an effect that increments the counter by 1\n const increment = Ref.update(ref, (n) => n + 1);\n\n // Create an array of 1,000 increment effects\n const tasks = Array.from({ length: 1000 }, () => increment);\n\n // Run all 1,000 effects concurrently\n yield* Effect.all(tasks, { concurrency: \"unbounded\" });\n\n // Get the final value of the counter\n return yield* Ref.get(ref);\n});\n\n// The result will always be 1000\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Final counter value: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "The anti-pattern is using a standard JavaScript variable for shared state. The following example is not guaranteed to produce the correct result.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This is a classic race condition.\nconst programWithRaceCondition = Effect.gen(function* () {\n let count = 0; // A plain, mutable variable\n\n // An effect that reads, increments, and writes the variable\n const increment = Effect.sync(() => {\n const current = count;\n // Another fiber could run between this read and the write below!\n count = current + 1;\n });\n\n const tasks = Array.from({ length: 1000 }, () => increment);\n\n yield* Effect.all(tasks, { concurrency: \"unbounded\" });\n\n return count;\n});\n\n// The result is unpredictable and will likely be less than 1000.\nEffect.runPromise(programWithRaceCondition).then(console.log);\n```", - "explanation": "Directly using a mutable variable (e.g., `let myState = ...`) in a concurrent system is dangerous. Multiple fibers could try to read and write to it at the same time, leading to race conditions and unpredictable results.\n\n`Ref` solves this by wrapping the state in a fiber-safe container. It's like a synchronized, in-memory cell. All operations on a `Ref` are atomic effects, guaranteeing that updates are applied correctly without being interrupted or interleaved with other updates. This eliminates race conditions and ensures data integrity.\n\n---", - "content": "## Guideline\n\nWhen you need to share mutable state between different concurrent fibers, create a `Ref`. Use `Ref.get` to read the value and `Ref.update` or `Ref.set` to modify it. All operations on a `Ref` are atomic.\n\n---\n\n## Rationale\n\nDirectly using a mutable variable (e.g., `let myState = ...`) in a concurrent system is dangerous. Multiple fibers could try to read and write to it at the same time, leading to race conditions and unpredictable results.\n\n`Ref` solves this by wrapping the state in a fiber-safe container. It's like a synchronized, in-memory cell. All operations on a `Ref` are atomic effects, guaranteeing that updates are applied correctly without being interrupted or interleaved with other updates. This eliminates race conditions and ensures data integrity.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Ref } from \"effect\";\n\nconst program = Effect.gen(function* () {\n // Create a new Ref with an initial value of 0\n const ref = yield* Ref.make(0);\n\n // Define an effect that increments the counter by 1\n const increment = Ref.update(ref, (n) => n + 1);\n\n // Create an array of 1,000 increment effects\n const tasks = Array.from({ length: 1000 }, () => increment);\n\n // Run all 1,000 effects concurrently\n yield* Effect.all(tasks, { concurrency: \"unbounded\" });\n\n // Get the final value of the counter\n return yield* Ref.get(ref);\n});\n\n// The result will always be 1000\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Final counter value: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nThe anti-pattern is using a standard JavaScript variable for shared state. The following example is not guaranteed to produce the correct result.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This is a classic race condition.\nconst programWithRaceCondition = Effect.gen(function* () {\n let count = 0; // A plain, mutable variable\n\n // An effect that reads, increments, and writes the variable\n const increment = Effect.sync(() => {\n const current = count;\n // Another fiber could run between this read and the write below!\n count = current + 1;\n });\n\n const tasks = Array.from({ length: 1000 }, () => increment);\n\n yield* Effect.all(tasks, { concurrency: \"unbounded\" });\n\n return count;\n});\n\n// The result is unpredictable and will likely be less than 1000.\nEffect.runPromise(programWithRaceCondition).then(console.log);\n```" - }, - { - "id": "data-ref", - "title": "Manage Shared State Safely with Ref", - "description": "Use Ref to safely manage shared, mutable state in concurrent and effectful programs.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Ref } from \"effect\";\n\n// Create a Ref with an initial value\nconst makeCounter = Ref.make(0);\n\n// Increment the counter atomically\nconst increment = makeCounter.pipe(\n Effect.flatMap((counter) => Ref.update(counter, (n) => n + 1))\n);\n\n// Read the current value\nconst getValue = makeCounter.pipe(\n Effect.flatMap((counter) => Ref.get(counter))\n);\n\n// Use Ref in a workflow\nconst program = Effect.gen(function* () {\n const counter = yield* Ref.make(0);\n yield* Ref.update(counter, (n) => n + 1);\n const value = yield* Ref.get(counter);\n yield* Effect.log(`Counter value: ${value}`);\n});\n```\n\n**Explanation:**\n\n- `Ref` is an atomic, mutable reference for effectful and concurrent code.\n- All operations are safe, composable, and free of race conditions.\n- Use `Ref` for counters, caches, or any shared mutable state.", - "antiPattern": "Using plain variables or objects for shared state in concurrent or async code, which can lead to race conditions, bugs, and unpredictable behavior.", - "explanation": "Managing shared state with plain variables or objects is unsafe in concurrent or asynchronous code. \n`Ref` ensures all updates are atomic and free of race conditions, making your code robust and predictable.", - "content": "# Manage Shared State Safely with `Ref`\n\n## Guideline\n\nUse the `Ref` data type to model shared, mutable state in a concurrent environment. \n`Ref` provides atomic, thread-safe operations for reading and updating state in effectful programs.\n\n## Rationale\n\nManaging shared state with plain variables or objects is unsafe in concurrent or asynchronous code. \n`Ref` ensures all updates are atomic and free of race conditions, making your code robust and predictable.\n\n## Good Example\n\n```typescript\nimport { Effect, Ref } from \"effect\";\n\n// Create a Ref with an initial value\nconst makeCounter = Ref.make(0);\n\n// Increment the counter atomically\nconst increment = makeCounter.pipe(\n Effect.flatMap((counter) => Ref.update(counter, (n) => n + 1))\n);\n\n// Read the current value\nconst getValue = makeCounter.pipe(\n Effect.flatMap((counter) => Ref.get(counter))\n);\n\n// Use Ref in a workflow\nconst program = Effect.gen(function* () {\n const counter = yield* Ref.make(0);\n yield* Ref.update(counter, (n) => n + 1);\n const value = yield* Ref.get(counter);\n yield* Effect.log(`Counter value: ${value}`);\n});\n```\n\n**Explanation:**\n\n- `Ref` is an atomic, mutable reference for effectful and concurrent code.\n- All operations are safe, composable, and free of race conditions.\n- Use `Ref` for counters, caches, or any shared mutable state.\n\n## Anti-Pattern\n\nUsing plain variables or objects for shared state in concurrent or async code, which can lead to race conditions, bugs, and unpredictable behavior." - }, - { - "id": "manual-scope-management", - "title": "Manually Manage Lifecycles with `Scope`", - "description": "Use `Effect.scope` and `Scope.addFinalizer` for fine-grained control over resource cleanup.", - "skillLevel": "advanced", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Console } from \"effect\";\n\n// Mocking a complex file operation\nconst openFile = (path: string) =>\n Effect.succeed({ path, handle: Math.random() }).pipe(\n Effect.tap((f) => Effect.log(`Opened ${f.path}`))\n );\nconst createTempFile = (path: string) =>\n Effect.succeed({ path: `${path}.tmp`, handle: Math.random() }).pipe(\n Effect.tap((f) => Effect.log(`Created temp file ${f.path}`))\n );\nconst closeFile = (file: { path: string }) =>\n Effect.sync(() => Effect.log(`Closed ${file.path}`));\nconst deleteFile = (file: { path: string }) =>\n Effect.sync(() => Effect.log(`Deleted ${file.path}`));\n\n// This program acquires two resources (a file and a temp file)\n// and ensures both are cleaned up correctly using acquireRelease.\nconst program = Effect.gen(function* () {\n const file = yield* Effect.acquireRelease(openFile(\"data.csv\"), (f) =>\n closeFile(f)\n );\n\n const tempFile = yield* Effect.acquireRelease(\n createTempFile(\"data.csv\"),\n (f) => deleteFile(f)\n );\n\n yield* Effect.log(\"...writing data from temp file to main file...\");\n});\n\n// Run the program with a scope\nEffect.runPromise(Effect.scoped(program));\n\n/*\nOutput (note the LIFO cleanup order):\nOpened data.csv\nCreated temp file data.csv.tmp\n...writing data from temp file to main file...\nDeleted data.csv.tmp\nClosed data.csv\n*/\n```\n\n**Explanation:**\n`Effect.scope` creates a new `Scope` and provides it to the `program`. Inside `program`, we access this `Scope` and use `addFinalizer` to register cleanup actions immediately after acquiring each resource. When `Effect.scope` finishes executing `program`, it closes the scope, which in turn executes all registered finalizers in the reverse order of their addition.", - "antiPattern": "Attempting to manage multiple, interdependent resource cleanups using nested `try...finally` blocks. This leads to a \"pyramid of doom,\" is difficult to read, and remains unsafe in the face of interruptions.\n\n```typescript\n// ANTI-PATTERN: Nested, unsafe, and hard to read\nasync function complexOperation() {\n const file = await openFilePromise(); // acquire 1\n try {\n const tempFile = await createTempFilePromise(); // acquire 2\n try {\n await doWorkPromise(file, tempFile); // use\n } finally {\n // This block may not run on interruption!\n await deleteFilePromise(tempFile); // release 2\n }\n } finally {\n // This block may also not run on interruption!\n await closeFilePromise(file); // release 1\n }\n}\n```", - "explanation": "While `Effect.acquireRelease` and `Layer.scoped` are sufficient for most use cases, sometimes you need more control. This pattern is essential when:\n\n1. A single logical operation acquires multiple resources that need independent cleanup.\n2. You are building a custom, complex `Layer` that orchestrates several dependent resources.\n3. You need to understand the fundamental mechanism that powers all of Effect's resource management.\n\nBy interacting with `Scope` directly, you gain precise, imperative-style control over resource cleanup within Effect's declarative, functional framework. Finalizers added to a scope are guaranteed to run in Last-In-First-Out (LIFO) order when the scope is closed.", - "content": "# Manually Manage Lifecycles with `Scope`\n\n## Guideline\n\nFor complex scenarios where a resource's lifecycle doesn't fit a simple `acquireRelease` pattern, use `Effect.scope` to create a boundary for finalizers. Inside this boundary, you can access the `Scope` service and manually register cleanup actions using `Scope.addFinalizer`.\n\n## Rationale\n\nWhile `Effect.acquireRelease` and `Layer.scoped` are sufficient for most use cases, sometimes you need more control. This pattern is essential when:\n\n1. A single logical operation acquires multiple resources that need independent cleanup.\n2. You are building a custom, complex `Layer` that orchestrates several dependent resources.\n3. You need to understand the fundamental mechanism that powers all of Effect's resource management.\n\nBy interacting with `Scope` directly, you gain precise, imperative-style control over resource cleanup within Effect's declarative, functional framework. Finalizers added to a scope are guaranteed to run in Last-In-First-Out (LIFO) order when the scope is closed.\n\n## Good Example\n\n```typescript\nimport { Effect, Console } from \"effect\";\n\n// Mocking a complex file operation\nconst openFile = (path: string) =>\n Effect.succeed({ path, handle: Math.random() }).pipe(\n Effect.tap((f) => Effect.log(`Opened ${f.path}`))\n );\nconst createTempFile = (path: string) =>\n Effect.succeed({ path: `${path}.tmp`, handle: Math.random() }).pipe(\n Effect.tap((f) => Effect.log(`Created temp file ${f.path}`))\n );\nconst closeFile = (file: { path: string }) =>\n Effect.sync(() => Effect.log(`Closed ${file.path}`));\nconst deleteFile = (file: { path: string }) =>\n Effect.sync(() => Effect.log(`Deleted ${file.path}`));\n\n// This program acquires two resources (a file and a temp file)\n// and ensures both are cleaned up correctly using acquireRelease.\nconst program = Effect.gen(function* () {\n const file = yield* Effect.acquireRelease(openFile(\"data.csv\"), (f) =>\n closeFile(f)\n );\n\n const tempFile = yield* Effect.acquireRelease(\n createTempFile(\"data.csv\"),\n (f) => deleteFile(f)\n );\n\n yield* Effect.log(\"...writing data from temp file to main file...\");\n});\n\n// Run the program with a scope\nEffect.runPromise(Effect.scoped(program));\n\n/*\nOutput (note the LIFO cleanup order):\nOpened data.csv\nCreated temp file data.csv.tmp\n...writing data from temp file to main file...\nDeleted data.csv.tmp\nClosed data.csv\n*/\n```\n\n**Explanation:**\n`Effect.scope` creates a new `Scope` and provides it to the `program`. Inside `program`, we access this `Scope` and use `addFinalizer` to register cleanup actions immediately after acquiring each resource. When `Effect.scope` finishes executing `program`, it closes the scope, which in turn executes all registered finalizers in the reverse order of their addition.\n\n## Anti-Pattern\n\nAttempting to manage multiple, interdependent resource cleanups using nested `try...finally` blocks. This leads to a \"pyramid of doom,\" is difficult to read, and remains unsafe in the face of interruptions.\n\n```typescript\n// ANTI-PATTERN: Nested, unsafe, and hard to read\nasync function complexOperation() {\n const file = await openFilePromise(); // acquire 1\n try {\n const tempFile = await createTempFilePromise(); // acquire 2\n try {\n await doWorkPromise(file, tempFile); // use\n } finally {\n // This block may not run on interruption!\n await deleteFilePromise(tempFile); // release 2\n }\n } finally {\n // This block may also not run on interruption!\n await closeFilePromise(file); // release 1\n }\n}\n```" - }, - { - "id": "combinator-foreach-all", - "title": "Mapping and Chaining over Collections with forEach and all", - "description": "Use forEach and all to process collections of values with effectful functions, collecting results in a type-safe and composable way.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Either, Option, Stream } from \"effect\";\n\n// Effect: Apply an effectful function to each item in an array\nconst numbers = [1, 2, 3];\nconst effect = Effect.forEach(numbers, (n) => Effect.succeed(n * 2));\n// Effect\n\n// Effect: Run multiple effects in parallel and collect results\nconst effects = [Effect.succeed(1), Effect.succeed(2)];\nconst allEffect = Effect.all(effects, { concurrency: \"unbounded\" }); // Effect<[1, 2]>\n\n// Option: Map over a collection of options and collect only the Some values\nconst options = [Option.some(1), Option.none(), Option.some(3)];\nconst filtered = options.filter(Option.isSome).map((o) => o.value); // [1, 3]\n\n// Either: Collect all Right values from a collection of Eithers\nconst eithers = [Either.right(1), Either.left(\"fail\"), Either.right(3)];\nconst rights = eithers.filter(Either.isRight); // [Either.Right(1), Either.Right(3)]\n\n// Stream: Map and flatten a stream of arrays\nconst stream = Stream.fromIterable([\n [1, 2],\n [3, 4],\n]).pipe(Stream.flatMap((arr) => Stream.fromIterable(arr))); // Stream\n```\n\n**Explanation:** \n`forEach` and `all` let you process collections in a way that is composable, type-safe, and often parallel. \nThey handle errors and context automatically, and can be used for batch jobs, parallel requests, or data transformations.", - "antiPattern": "Using manual loops (`for`, `forEach`, etc.) with side effects, or collecting results imperatively, which breaks composability and loses error/context handling.", - "explanation": "Batch and parallel processing are common in real-world applications. \nThese combinators let you express \"do this for every item\" declaratively, without manual loops or imperative control flow, and they preserve error handling and context propagation.", - "content": "# Mapping and Chaining over Collections with `forEach` and `all`\n\n## Guideline\n\nUse the `forEach` and `all` combinators to apply an effectful function to every item in a collection and combine the results. \nThis enables you to process lists, arrays, or other collections in a type-safe, composable, and often parallel way.\n\n## Rationale\n\nBatch and parallel processing are common in real-world applications. \nThese combinators let you express \"do this for every item\" declaratively, without manual loops or imperative control flow, and they preserve error handling and context propagation.\n\n## Good Example\n\n```typescript\nimport { Effect, Either, Option, Stream } from \"effect\";\n\n// Effect: Apply an effectful function to each item in an array\nconst numbers = [1, 2, 3];\nconst effect = Effect.forEach(numbers, (n) => Effect.succeed(n * 2));\n// Effect\n\n// Effect: Run multiple effects in parallel and collect results\nconst effects = [Effect.succeed(1), Effect.succeed(2)];\nconst allEffect = Effect.all(effects, { concurrency: \"unbounded\" }); // Effect<[1, 2]>\n\n// Option: Map over a collection of options and collect only the Some values\nconst options = [Option.some(1), Option.none(), Option.some(3)];\nconst filtered = options.filter(Option.isSome).map((o) => o.value); // [1, 3]\n\n// Either: Collect all Right values from a collection of Eithers\nconst eithers = [Either.right(1), Either.left(\"fail\"), Either.right(3)];\nconst rights = eithers.filter(Either.isRight); // [Either.Right(1), Either.Right(3)]\n\n// Stream: Map and flatten a stream of arrays\nconst stream = Stream.fromIterable([\n [1, 2],\n [3, 4],\n]).pipe(Stream.flatMap((arr) => Stream.fromIterable(arr))); // Stream\n```\n\n**Explanation:** \n`forEach` and `all` let you process collections in a way that is composable, type-safe, and often parallel. \nThey handle errors and context automatically, and can be used for batch jobs, parallel requests, or data transformations.\n\n## Anti-Pattern\n\nUsing manual loops (`for`, `forEach`, etc.) with side effects, or collecting results imperatively, which breaks composability and loses error/context handling." - }, - { - "id": "mapping-errors-to-fit-your-domain", - "title": "Mapping Errors to Fit Your Domain", - "description": "Use Effect.mapError to transform errors and create clean architectural boundaries between layers.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "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.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Low-level, specific errors from the database layer\nclass ConnectionError extends Data.TaggedError(\"ConnectionError\") {}\nclass QueryError extends Data.TaggedError(\"QueryError\") {}\n\n// A generic error for the repository layer\nclass RepositoryError extends Data.TaggedError(\"RepositoryError\")<{\n readonly cause: unknown;\n}> {}\n\n// The inner service\nconst dbQuery = (): Effect.Effect<\n { name: string },\n ConnectionError | QueryError\n> => Effect.fail(new ConnectionError());\n\n// The outer service uses `mapError` to create a clean boundary.\n// Its public signature only exposes `RepositoryError`.\nconst findUser = (): Effect.Effect<{ name: string }, RepositoryError> =>\n dbQuery().pipe(\n Effect.mapError((error) => new RepositoryError({ cause: error }))\n );\n\n// Demonstrate the error mapping\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Attempting to find user...\");\n\n try {\n const user = yield* findUser();\n yield* Effect.logInfo(`Found user: ${user.name}`);\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof RepositoryError) {\n yield* Effect.logInfo(`Repository error occurred: ${error._tag}`);\n if (\n error.cause instanceof ConnectionError ||\n error.cause instanceof QueryError\n ) {\n yield* Effect.logInfo(`Original cause: ${error.cause._tag}`);\n }\n } else {\n yield* Effect.logInfo(`Unexpected error: ${error}`);\n }\n })\n )\n);\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "Allowing low-level, implementation-specific errors to \"leak\" out of a service's public API. This creates tight coupling between layers.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { ConnectionError, QueryError } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This function's error channel is \"leaky\".\n// It exposes the internal implementation details of the database.\nconst findUserUnsafely = (): Effect.Effect<\n { name: string },\n ConnectionError | QueryError // <-- Leaky abstraction\n> => {\n // ... logic that calls the database\n return Effect.fail(new ConnectionError());\n};\n\n// Now, any code that calls `findUserUnsafely` has to know about and handle\n// both `ConnectionError` and `QueryError`. If we change the database,\n// all of that calling code might have to change too.\n```", - "explanation": "This pattern is essential for creating clean architectural boundaries and preventing \"leaky abstractions.\" An outer layer of your application (e.g., a `UserService`) should not expose the internal failure details of the layers it depends on (e.g., a `Database` that can fail with `ConnectionError` or `QueryError`).\n\nBy using `Effect.mapError`, the outer layer can define its own, more abstract error type (like `RepositoryError`) and map all the specific, low-level errors into it. This decouples the layers. If you later swap your database implementation, you only need to update the mapping logic within the repository layer; none of the code that _uses_ the repository needs to change.\n\n---", - "content": "## Guideline\n\nWhen an inner service can fail with specific errors, use `Effect.mapError` in the outer service to catch those specific errors and transform them into a more general error suitable for its own domain.\n\n---\n\n## Rationale\n\nThis pattern is essential for creating clean architectural boundaries and preventing \"leaky abstractions.\" An outer layer of your application (e.g., a `UserService`) should not expose the internal failure details of the layers it depends on (e.g., a `Database` that can fail with `ConnectionError` or `QueryError`).\n\nBy using `Effect.mapError`, the outer layer can define its own, more abstract error type (like `RepositoryError`) and map all the specific, low-level errors into it. This decouples the layers. If you later swap your database implementation, you only need to update the mapping logic within the repository layer; none of the code that _uses_ the repository needs to change.\n\n---\n\n## Good Example\n\nA `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.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Low-level, specific errors from the database layer\nclass ConnectionError extends Data.TaggedError(\"ConnectionError\") {}\nclass QueryError extends Data.TaggedError(\"QueryError\") {}\n\n// A generic error for the repository layer\nclass RepositoryError extends Data.TaggedError(\"RepositoryError\")<{\n readonly cause: unknown;\n}> {}\n\n// The inner service\nconst dbQuery = (): Effect.Effect<\n { name: string },\n ConnectionError | QueryError\n> => Effect.fail(new ConnectionError());\n\n// The outer service uses `mapError` to create a clean boundary.\n// Its public signature only exposes `RepositoryError`.\nconst findUser = (): Effect.Effect<{ name: string }, RepositoryError> =>\n dbQuery().pipe(\n Effect.mapError((error) => new RepositoryError({ cause: error }))\n );\n\n// Demonstrate the error mapping\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Attempting to find user...\");\n\n try {\n const user = yield* findUser();\n yield* Effect.logInfo(`Found user: ${user.name}`);\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof RepositoryError) {\n yield* Effect.logInfo(`Repository error occurred: ${error._tag}`);\n if (\n error.cause instanceof ConnectionError ||\n error.cause instanceof QueryError\n ) {\n yield* Effect.logInfo(`Original cause: ${error.cause._tag}`);\n }\n } else {\n yield* Effect.logInfo(`Unexpected error: ${error}`);\n }\n })\n )\n);\n\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nAllowing low-level, implementation-specific errors to \"leak\" out of a service's public API. This creates tight coupling between layers.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { ConnectionError, QueryError } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This function's error channel is \"leaky\".\n// It exposes the internal implementation details of the database.\nconst findUserUnsafely = (): Effect.Effect<\n { name: string },\n ConnectionError | QueryError // <-- Leaky abstraction\n> => {\n // ... logic that calls the database\n return Effect.fail(new ConnectionError());\n};\n\n// Now, any code that calls `findUserUnsafely` has to know about and handle\n// both `ConnectionError` and `QueryError`. If we change the database,\n// all of that calling code might have to change too.\n```" - }, - { - "id": "pattern-match", - "title": "Matching on Success and Failure with match", - "description": "Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively.", - "skillLevel": "beginner", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Handle both success and failure\nconst effect = Effect.fail(\"Oops!\").pipe(\n Effect.match({\n onFailure: (err) => `Error: ${err}`,\n onSuccess: (value) => `Success: ${value}`,\n })\n); // Effect\n\n// Option: Handle Some and None cases\nconst option = Option.some(42).pipe(\n Option.match({\n onNone: () => \"No value\",\n onSome: (n) => `Value: ${n}`,\n })\n); // string\n\n// Either: Handle Left and Right cases\nconst either = Either.left(\"fail\").pipe(\n Either.match({\n onLeft: (err) => `Error: ${err}`,\n onRight: (value) => `Value: ${value}`,\n })\n); // string\n```\n\n**Explanation:**\n\n- `Effect.match` lets you handle both the error and success channels in one place.\n- `Option.match` and `Either.match` let you handle all possible cases for these types, making your code exhaustive and safe.", - "antiPattern": "Using nested if/else or switch statements to check for success/failure, or ignoring possible error/none/left cases, which leads to brittle and less readable code.", - "explanation": "Pattern matching with `match` keeps your code clear and type-safe, ensuring you handle all possible outcomes. \nIt avoids scattered if/else or switch statements and makes your intent explicit.", - "content": "# Matching on Success and Failure with `match`\n\n## Guideline\n\nUse the `match` combinator to handle both success and failure cases in a single, declarative place. \nThis works for `Effect`, `Option`, and `Either`, and is the foundation for robust, readable error handling and branching.\n\n## Rationale\n\nPattern matching with `match` keeps your code clear and type-safe, ensuring you handle all possible outcomes. \nIt avoids scattered if/else or switch statements and makes your intent explicit.\n\n## Good Example\n\n```typescript\nimport { Effect, Option, Either } from \"effect\";\n\n// Effect: Handle both success and failure\nconst effect = Effect.fail(\"Oops!\").pipe(\n Effect.match({\n onFailure: (err) => `Error: ${err}`,\n onSuccess: (value) => `Success: ${value}`,\n })\n); // Effect\n\n// Option: Handle Some and None cases\nconst option = Option.some(42).pipe(\n Option.match({\n onNone: () => \"No value\",\n onSome: (n) => `Value: ${n}`,\n })\n); // string\n\n// Either: Handle Left and Right cases\nconst either = Either.left(\"fail\").pipe(\n Either.match({\n onLeft: (err) => `Error: ${err}`,\n onRight: (value) => `Value: ${value}`,\n })\n); // string\n```\n\n**Explanation:**\n\n- `Effect.match` lets you handle both the error and success channels in one place.\n- `Option.match` and `Either.match` let you handle all possible cases for these types, making your code exhaustive and safe.\n\n## Anti-Pattern\n\nUsing nested if/else or switch statements to check for success/failure, or ignoring possible error/none/left cases, which leads to brittle and less readable code." - }, - { - "id": "pattern-matchtag", - "title": "Matching Tagged Unions with matchTag and matchTags", - "description": "Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define a tagged error type\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{}> {}\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n message: string;\n}> {}\n\ntype MyError = NotFoundError | ValidationError;\n\n// Effect: Match on specific error tags\nconst effect: Effect.Effect = Effect.fail(\n new ValidationError({ message: \"Invalid input\" }) as MyError\n).pipe(\n Effect.catchTags({\n NotFoundError: () => Effect.succeed(\"Not found!\"),\n ValidationError: (err) =>\n Effect.succeed(`Validation failed: ${err.message}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `matchTag` lets you branch on the specific tag of a tagged union or custom error type.\n- This is safer and more maintainable than using `instanceof` or manual property checks.", - "antiPattern": "Using `instanceof`, manual property checks, or switch statements to distinguish between cases, which is error-prone and less type-safe than declarative pattern matching.", - "explanation": "Tagged unions (a.k.a. algebraic data types or ADTs) are a powerful way to model domain logic. \nPattern matching on tags lets you handle each case explicitly, making your code robust, maintainable, and exhaustive.", - "content": "# Matching Tagged Unions with `matchTag` and `matchTags`\n\n## Guideline\n\nUse the `matchTag` and `matchTags` combinators to pattern match on specific cases of tagged unions or custom error types. \nThis enables precise, type-safe branching and is especially useful for handling domain-specific errors or ADTs.\n\n## Rationale\n\nTagged unions (a.k.a. algebraic data types or ADTs) are a powerful way to model domain logic. \nPattern matching on tags lets you handle each case explicitly, making your code robust, maintainable, and exhaustive.\n\n## Good Example\n\n```typescript\nimport { Data, Effect } from \"effect\";\n\n// Define a tagged error type\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{}> {}\nclass ValidationError extends Data.TaggedError(\"ValidationError\")<{\n message: string;\n}> {}\n\ntype MyError = NotFoundError | ValidationError;\n\n// Effect: Match on specific error tags\nconst effect: Effect.Effect = Effect.fail(\n new ValidationError({ message: \"Invalid input\" }) as MyError\n).pipe(\n Effect.catchTags({\n NotFoundError: () => Effect.succeed(\"Not found!\"),\n ValidationError: (err) =>\n Effect.succeed(`Validation failed: ${err.message}`),\n })\n); // Effect\n```\n\n**Explanation:**\n\n- `matchTag` lets you branch on the specific tag of a tagged union or custom error type.\n- This is safer and more maintainable than using `instanceof` or manual property checks.\n\n## Anti-Pattern\n\nUsing `instanceof`, manual property checks, or switch statements to distinguish between cases, which is error-prone and less type-safe than declarative pattern matching." - }, - { - "id": "pipeline-merge", - "title": "Merge Multiple Streams", - "description": "Use merge, concat, or zip to combine multiple streams based on your requirements.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "example": "```typescript\nimport { Effect, Stream, Duration, Chunk } from \"effect\"\n\n// ============================================\n// 1. Merge - interleave as items arrive\n// ============================================\n\nconst mergeExample = Effect.gen(function* () {\n // Two streams producing at different rates\n const fast = Stream.fromIterable([\"A1\", \"A2\", \"A3\"]).pipe(\n Stream.tap(() => Effect.sleep(\"100 millis\"))\n )\n\n const slow = Stream.fromIterable([\"B1\", \"B2\", \"B3\"]).pipe(\n Stream.tap(() => Effect.sleep(\"200 millis\"))\n )\n\n // Merge interleaves based on arrival time\n const merged = Stream.merge(fast, slow)\n\n yield* merged.pipe(\n Stream.tap((item) => Effect.log(`Received: ${item}`)),\n Stream.runDrain\n )\n // Output order depends on timing: A1, B1, A2, A3, B2, B3 (approximately)\n})\n\n// ============================================\n// 2. Merge all - combine many streams\n// ============================================\n\nconst mergeAllExample = Effect.gen(function* () {\n const streams = [\n Stream.fromIterable([1, 2, 3]),\n Stream.fromIterable([10, 20, 30]),\n Stream.fromIterable([100, 200, 300]),\n ]\n\n const merged = Stream.mergeAll(streams, { concurrency: 3 })\n\n const results = yield* merged.pipe(Stream.runCollect)\n yield* Effect.log(`Merged: ${Chunk.toReadonlyArray(results)}`)\n})\n\n// ============================================\n// 3. Concat - sequence streams\n// ============================================\n\nconst concatExample = Effect.gen(function* () {\n const first = Stream.fromIterable([1, 2, 3])\n const second = Stream.fromIterable([4, 5, 6])\n const third = Stream.fromIterable([7, 8, 9])\n\n // Concat waits for each stream to complete\n const sequential = Stream.concat(Stream.concat(first, second), third)\n\n const results = yield* sequential.pipe(Stream.runCollect)\n yield* Effect.log(`Concatenated: ${Chunk.toReadonlyArray(results)}`)\n // Always: [1, 2, 3, 4, 5, 6, 7, 8, 9]\n})\n\n// ============================================\n// 4. Zip - pair items from streams\n// ============================================\n\nconst zipExample = Effect.gen(function* () {\n const names = Stream.fromIterable([\"Alice\", \"Bob\", \"Charlie\"])\n const ages = Stream.fromIterable([30, 25, 35])\n\n // Zip pairs items by position\n const zipped = Stream.zip(names, ages)\n\n yield* zipped.pipe(\n Stream.tap(([name, age]) => Effect.log(`${name} is ${age} years old`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 5. ZipWith - pair and transform\n// ============================================\n\nconst zipWithExample = Effect.gen(function* () {\n const prices = Stream.fromIterable([100, 200, 150])\n const quantities = Stream.fromIterable([2, 1, 3])\n\n // Zip and calculate total\n const totals = Stream.zipWith(prices, quantities, (price, qty) => ({\n price,\n quantity: qty,\n total: price * qty,\n }))\n\n yield* totals.pipe(\n Stream.tap((item) => Effect.log(`${item.quantity}x @ $${item.price} = $${item.total}`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 6. ZipLatest - combine with latest values\n// ============================================\n\nconst zipLatestExample = Effect.gen(function* () {\n // Simulate different update rates\n const temperature = Stream.fromIterable([20, 21, 22, 23]).pipe(\n Stream.tap(() => Effect.sleep(\"100 millis\"))\n )\n\n const humidity = Stream.fromIterable([50, 55, 60]).pipe(\n Stream.tap(() => Effect.sleep(\"150 millis\"))\n )\n\n // ZipLatest always uses the latest value from each stream\n const combined = Stream.zipLatest(temperature, humidity)\n\n yield* combined.pipe(\n Stream.tap(([temp, hum]) => Effect.log(`Temp: ${temp}°C, Humidity: ${hum}%`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 7. Practical example: Merge event sources\n// ============================================\n\ninterface Event {\n source: string\n type: string\n data: unknown\n}\n\nconst mergeEventSources = Effect.gen(function* () {\n // Simulate multiple event sources\n const mouseEvents = Stream.fromIterable([\n { source: \"mouse\", type: \"click\", data: { x: 100, y: 200 } },\n { source: \"mouse\", type: \"move\", data: { x: 150, y: 250 } },\n ] as Event[])\n\n const keyboardEvents = Stream.fromIterable([\n { source: \"keyboard\", type: \"keydown\", data: { key: \"Enter\" } },\n { source: \"keyboard\", type: \"keyup\", data: { key: \"Enter\" } },\n ] as Event[])\n\n const networkEvents = Stream.fromIterable([\n { source: \"network\", type: \"response\", data: { status: 200 } },\n ] as Event[])\n\n // Merge all event sources\n const allEvents = Stream.mergeAll([mouseEvents, keyboardEvents, networkEvents])\n\n yield* allEvents.pipe(\n Stream.tap((event) =>\n Effect.log(`[${event.source}] ${event.type}: ${JSON.stringify(event.data)}`)\n ),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 8. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Merge Example ===\")\n yield* mergeExample\n\n yield* Effect.log(\"\\n=== Concat Example ===\")\n yield* concatExample\n\n yield* Effect.log(\"\\n=== Zip Example ===\")\n yield* zipExample\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Merging streams enables:\n\n1. **Aggregation** - Combine data from multiple sources\n2. **Correlation** - Match related data\n3. **Multiplexing** - Single consumer for multiple producers\n4. **Comparison** - Process streams side by side\n\n---", - "content": "## Guideline\n\nChoose the right combination strategy: merge for interleaving, concat for sequencing, or zip for pairing items.\n\n---\n\n## Rationale\n\nMerging streams enables:\n\n1. **Aggregation** - Combine data from multiple sources\n2. **Correlation** - Match related data\n3. **Multiplexing** - Single consumer for multiple producers\n4. **Comparison** - Process streams side by side\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Duration, Chunk } from \"effect\"\n\n// ============================================\n// 1. Merge - interleave as items arrive\n// ============================================\n\nconst mergeExample = Effect.gen(function* () {\n // Two streams producing at different rates\n const fast = Stream.fromIterable([\"A1\", \"A2\", \"A3\"]).pipe(\n Stream.tap(() => Effect.sleep(\"100 millis\"))\n )\n\n const slow = Stream.fromIterable([\"B1\", \"B2\", \"B3\"]).pipe(\n Stream.tap(() => Effect.sleep(\"200 millis\"))\n )\n\n // Merge interleaves based on arrival time\n const merged = Stream.merge(fast, slow)\n\n yield* merged.pipe(\n Stream.tap((item) => Effect.log(`Received: ${item}`)),\n Stream.runDrain\n )\n // Output order depends on timing: A1, B1, A2, A3, B2, B3 (approximately)\n})\n\n// ============================================\n// 2. Merge all - combine many streams\n// ============================================\n\nconst mergeAllExample = Effect.gen(function* () {\n const streams = [\n Stream.fromIterable([1, 2, 3]),\n Stream.fromIterable([10, 20, 30]),\n Stream.fromIterable([100, 200, 300]),\n ]\n\n const merged = Stream.mergeAll(streams, { concurrency: 3 })\n\n const results = yield* merged.pipe(Stream.runCollect)\n yield* Effect.log(`Merged: ${Chunk.toReadonlyArray(results)}`)\n})\n\n// ============================================\n// 3. Concat - sequence streams\n// ============================================\n\nconst concatExample = Effect.gen(function* () {\n const first = Stream.fromIterable([1, 2, 3])\n const second = Stream.fromIterable([4, 5, 6])\n const third = Stream.fromIterable([7, 8, 9])\n\n // Concat waits for each stream to complete\n const sequential = Stream.concat(Stream.concat(first, second), third)\n\n const results = yield* sequential.pipe(Stream.runCollect)\n yield* Effect.log(`Concatenated: ${Chunk.toReadonlyArray(results)}`)\n // Always: [1, 2, 3, 4, 5, 6, 7, 8, 9]\n})\n\n// ============================================\n// 4. Zip - pair items from streams\n// ============================================\n\nconst zipExample = Effect.gen(function* () {\n const names = Stream.fromIterable([\"Alice\", \"Bob\", \"Charlie\"])\n const ages = Stream.fromIterable([30, 25, 35])\n\n // Zip pairs items by position\n const zipped = Stream.zip(names, ages)\n\n yield* zipped.pipe(\n Stream.tap(([name, age]) => Effect.log(`${name} is ${age} years old`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 5. ZipWith - pair and transform\n// ============================================\n\nconst zipWithExample = Effect.gen(function* () {\n const prices = Stream.fromIterable([100, 200, 150])\n const quantities = Stream.fromIterable([2, 1, 3])\n\n // Zip and calculate total\n const totals = Stream.zipWith(prices, quantities, (price, qty) => ({\n price,\n quantity: qty,\n total: price * qty,\n }))\n\n yield* totals.pipe(\n Stream.tap((item) => Effect.log(`${item.quantity}x @ $${item.price} = $${item.total}`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 6. ZipLatest - combine with latest values\n// ============================================\n\nconst zipLatestExample = Effect.gen(function* () {\n // Simulate different update rates\n const temperature = Stream.fromIterable([20, 21, 22, 23]).pipe(\n Stream.tap(() => Effect.sleep(\"100 millis\"))\n )\n\n const humidity = Stream.fromIterable([50, 55, 60]).pipe(\n Stream.tap(() => Effect.sleep(\"150 millis\"))\n )\n\n // ZipLatest always uses the latest value from each stream\n const combined = Stream.zipLatest(temperature, humidity)\n\n yield* combined.pipe(\n Stream.tap(([temp, hum]) => Effect.log(`Temp: ${temp}°C, Humidity: ${hum}%`)),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 7. Practical example: Merge event sources\n// ============================================\n\ninterface Event {\n source: string\n type: string\n data: unknown\n}\n\nconst mergeEventSources = Effect.gen(function* () {\n // Simulate multiple event sources\n const mouseEvents = Stream.fromIterable([\n { source: \"mouse\", type: \"click\", data: { x: 100, y: 200 } },\n { source: \"mouse\", type: \"move\", data: { x: 150, y: 250 } },\n ] as Event[])\n\n const keyboardEvents = Stream.fromIterable([\n { source: \"keyboard\", type: \"keydown\", data: { key: \"Enter\" } },\n { source: \"keyboard\", type: \"keyup\", data: { key: \"Enter\" } },\n ] as Event[])\n\n const networkEvents = Stream.fromIterable([\n { source: \"network\", type: \"response\", data: { status: 200 } },\n ] as Event[])\n\n // Merge all event sources\n const allEvents = Stream.mergeAll([mouseEvents, keyboardEvents, networkEvents])\n\n yield* allEvents.pipe(\n Stream.tap((event) =>\n Effect.log(`[${event.source}] ${event.type}: ${JSON.stringify(event.data)}`)\n ),\n Stream.runDrain\n )\n})\n\n// ============================================\n// 8. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Merge Example ===\")\n yield* mergeExample\n\n yield* Effect.log(\"\\n=== Concat Example ===\")\n yield* concatExample\n\n yield* Effect.log(\"\\n=== Zip Example ===\")\n yield* zipExample\n})\n\nEffect.runPromise(program)\n```\n\n## Combination Strategies\n\n| Strategy | Behavior | Use When |\n|----------|----------|----------|\n| **Merge** | Interleave by arrival | Real-time aggregation |\n| **Concat** | Sequential | Ordered processing |\n| **Zip** | Pair by position | Correlated data |\n| **ZipLatest** | Pair with latest | Different rates |\n\n## Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `Stream.merge` | Merge two streams |\n| `Stream.mergeAll` | Merge many streams |\n| `Stream.concat` | Sequential combination |\n| `Stream.zip` | Pair by position |\n| `Stream.zipWith` | Pair and transform |\n| `Stream.zipLatest` | Pair with latest values |" - }, - { - "id": "mocking-dependencies-in-tests", - "title": "Mocking Dependencies in Tests", - "description": "Provide mock service implementations via a test-specific Layer to isolate the unit under test.", - "skillLevel": "intermediate", - "useCases": [ - "testing" - ], - "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.\n\n```typescript\nimport { Effect, Layer } from \"effect\";\n\n// --- The Services ---\ninterface EmailClientService {\n send: (address: string, body: string) => Effect.Effect;\n}\n\nclass EmailClient extends Effect.Service()(\"EmailClient\", {\n sync: () => ({\n send: (address: string, body: string) =>\n Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)),\n }),\n}) {}\n\ninterface NotifierService {\n notifyUser: (userId: number, message: string) => Effect.Effect;\n}\n\nclass Notifier extends Effect.Service()(\"Notifier\", {\n effect: Effect.gen(function* () {\n const emailClient = yield* EmailClient;\n return {\n notifyUser: (userId: number, message: string) =>\n emailClient.send(`user-${userId}@example.com`, message),\n };\n }),\n dependencies: [EmailClient.Default],\n}) {}\n\n// Create a program that uses the Notifier service\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Using default EmailClient implementation...\");\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"Your invoice is ready.\");\n\n // Create mock EmailClient that logs differently\n yield* Effect.log(\"\\nUsing mock EmailClient implementation...\");\n const mockEmailClient = Layer.succeed(EmailClient, {\n send: (address: string, body: string) =>\n // Directly return the Effect.log without nesting it in Effect.sync\n Effect.log(`MOCK: Would send to ${address} with body: ${body}`),\n } as EmailClientService);\n\n // Run the same notification with mock client\n yield* Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"Your invoice is ready.\");\n }).pipe(Effect.provide(mockEmailClient));\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, Notifier.Default));\n```\n\n---", - "antiPattern": "Testing your business logic using the \"live\" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email).\n\n```typescript\nimport { Effect } from \"effect\";\nimport { NotifierLive } from \"./somewhere\";\nimport { EmailClientLive } from \"./somewhere\"; // The REAL email client\n\n// ❌ WRONG: This test will try to send a real email.\nit(\"sends a real email\", () =>\n Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"This is a test email!\");\n }).pipe(\n Effect.provide(NotifierLive),\n Effect.provide(EmailClientLive), // Using the live layer makes this an integration test\n Effect.runPromise\n ));\n```", - "explanation": "The primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe.\n\nBy providing a mock `Layer` in your test, you replace a real dependency (like an `HttpClient` that makes network calls) with a fake one that returns predictable data. This provides several key benefits:\n\n- **Determinism:** Your tests always produce the same result, free from the flakiness of network or database connections.\n- **Speed:** Tests run instantly without waiting for slow I/O operations.\n- **Type Safety:** The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated.\n- **Explicitness:** The test setup clearly documents all the dependencies required for the code to run.\n\n---", - "content": "## Guideline\n\nTo test a piece of code in isolation, identify its service dependencies and provide mock implementations for them using a test-specific `Layer`. The most common way to create a mock layer is with `Layer.succeed(ServiceTag, mockImplementation)`.\n\n---\n\n## Rationale\n\nThe primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe.\n\nBy providing a mock `Layer` in your test, you replace a real dependency (like an `HttpClient` that makes network calls) with a fake one that returns predictable data. This provides several key benefits:\n\n- **Determinism:** Your tests always produce the same result, free from the flakiness of network or database connections.\n- **Speed:** Tests run instantly without waiting for slow I/O operations.\n- **Type Safety:** The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated.\n- **Explicitness:** The test setup clearly documents all the dependencies required for the code to run.\n\n---\n\n## Good Example\n\nWe 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.\n\n```typescript\nimport { Effect, Layer } from \"effect\";\n\n// --- The Services ---\ninterface EmailClientService {\n send: (address: string, body: string) => Effect.Effect;\n}\n\nclass EmailClient extends Effect.Service()(\"EmailClient\", {\n sync: () => ({\n send: (address: string, body: string) =>\n Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)),\n }),\n}) {}\n\ninterface NotifierService {\n notifyUser: (userId: number, message: string) => Effect.Effect;\n}\n\nclass Notifier extends Effect.Service()(\"Notifier\", {\n effect: Effect.gen(function* () {\n const emailClient = yield* EmailClient;\n return {\n notifyUser: (userId: number, message: string) =>\n emailClient.send(`user-${userId}@example.com`, message),\n };\n }),\n dependencies: [EmailClient.Default],\n}) {}\n\n// Create a program that uses the Notifier service\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Using default EmailClient implementation...\");\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"Your invoice is ready.\");\n\n // Create mock EmailClient that logs differently\n yield* Effect.log(\"\\nUsing mock EmailClient implementation...\");\n const mockEmailClient = Layer.succeed(EmailClient, {\n send: (address: string, body: string) =>\n // Directly return the Effect.log without nesting it in Effect.sync\n Effect.log(`MOCK: Would send to ${address} with body: ${body}`),\n } as EmailClientService);\n\n // Run the same notification with mock client\n yield* Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"Your invoice is ready.\");\n }).pipe(Effect.provide(mockEmailClient));\n});\n\n// Run the program\nEffect.runPromise(Effect.provide(program, Notifier.Default));\n```\n\n---\n\n## Anti-Pattern\n\nTesting your business logic using the \"live\" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email).\n\n```typescript\nimport { Effect } from \"effect\";\nimport { NotifierLive } from \"./somewhere\";\nimport { EmailClientLive } from \"./somewhere\"; // The REAL email client\n\n// ❌ WRONG: This test will try to send a real email.\nit(\"sends a real email\", () =>\n Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notifyUser(123, \"This is a test email!\");\n }).pipe(\n Effect.provide(NotifierLive),\n Effect.provide(EmailClientLive), // Using the live layer makes this an integration test\n Effect.runPromise\n ));\n```" - }, - { - "id": "model-dependencies-as-services", - "title": "Model Dependencies as Services", - "description": "Model dependencies as services.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Define Random service with production implementation as default\nexport class Random extends Effect.Service()(\"Random\", {\n // Default production implementation\n sync: () => ({\n next: Effect.sync(() => Math.random()),\n }),\n}) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const random = yield* Random;\n const value = yield* random.next;\n return value;\n});\n\n// Run with default implementation\nconst programWithLogging = Effect.gen(function* () {\n const value = yield* Effect.provide(program, Random.Default);\n yield* Effect.log(`Random value: ${value}`);\n return value;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \nBy modeling dependencies as services, you can easily substitute mocked or deterministic implementations for testing, leading to more reliable and predictable tests.", - "antiPattern": "Directly calling external APIs like `fetch` or using impure functions like `Math.random()` within your business logic. This tightly couples your logic to a specific implementation and makes it difficult to test.", - "explanation": "This pattern is the key to testability. It allows you to provide a `Live` implementation in production and a `Test` implementation (returning mock data) in your tests, making your code decoupled and reliable.", - "content": "# Model Dependencies as Services\n\n## Guideline\n\nRepresent any external dependency or distinct capability—from a database client to a simple UUID generator—as a service.\n\n## Rationale\n\nThis pattern is the key to testability. It allows you to provide a `Live` implementation in production and a `Test` implementation (returning mock data) in your tests, making your code decoupled and reliable.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define Random service with production implementation as default\nexport class Random extends Effect.Service()(\"Random\", {\n // Default production implementation\n sync: () => ({\n next: Effect.sync(() => Math.random()),\n }),\n}) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const random = yield* Random;\n const value = yield* random.next;\n return value;\n});\n\n// Run with default implementation\nconst programWithLogging = Effect.gen(function* () {\n const value = yield* Effect.provide(program, Random.Default);\n yield* Effect.log(`Random value: ${value}`);\n return value;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n**Explanation:** \nBy modeling dependencies as services, you can easily substitute mocked or deterministic implementations for testing, leading to more reliable and predictable tests.\n\n## Anti-Pattern\n\nDirectly calling external APIs like `fetch` or using impure functions like `Math.random()` within your business logic. This tightly couples your logic to a specific implementation and makes it difficult to test." - }, - { - "id": "model-optional-values-with-option", - "title": "Model Optional Values Safely with Option", - "description": "Use Option to explicitly model values that may be absent, avoiding null or undefined.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "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.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\n\nconst users: User[] = [\n { id: 1, name: \"Paul\" },\n { id: 2, name: \"Alex\" },\n];\n\n// This function safely returns an Option, not a User or null.\nconst findUserById = (id: number): Option.Option => {\n const user = users.find((u) => u.id === id);\n return Option.fromNullable(user); // A useful helper for existing APIs\n};\n\n// The caller MUST handle both cases.\nconst greeting = (id: number): string =>\n findUserById(id).pipe(\n Option.match({\n onNone: () => \"User not found.\",\n onSome: (user) => `Welcome, ${user.name}!`,\n })\n );\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(greeting(1)); // \"Welcome, Paul!\"\n yield* Effect.log(greeting(3)); // \"User not found.\"\n});\n\nEffect.runPromise(program);\n```", - "antiPattern": "The anti-pattern is returning a nullable type (e.g., User | null or User | undefined). This relies on the discipline of every single caller to perform a null check. Forgetting even one check can introduce a runtime error.\n\n```typescript\ninterface User {\n id: number;\n name: string;\n}\nconst users: User[] = [{ id: 1, name: \"Paul\" }];\n\n// ❌ WRONG: This function's return type is less safe.\nconst findUserUnsafely = (id: number): User | undefined => {\n return users.find((u) => u.id === id);\n};\n\nconst user = findUserUnsafely(3);\n\n// This will throw \"TypeError: Cannot read properties of undefined (reading 'name')\"\n// because the caller forgot to check if the user exists.\nconsole.log(`User's name is ${user.name}`);\n```", - "explanation": "Functions that can return a value or `null`/`undefined` are a primary source of runtime errors in TypeScript (`Cannot read properties of null`).\n\nThe `Option` type solves this by making the possibility of an absent value explicit in the type system. A function that returns `Option` cannot be mistaken for a function that returns `User`. The compiler forces you to handle the `None` case before you can access the value inside a `Some`, eliminating an entire class of bugs.\n\n---", - "content": "## Guideline\n\nRepresent values that may be absent with `Option`. Use `Option.some(value)` to represent a present value and `Option.none()` for an absent one. This creates a container that forces you to handle both possibilities.\n\n---\n\n## Rationale\n\nFunctions that can return a value or `null`/`undefined` are a primary source of runtime errors in TypeScript (`Cannot read properties of null`).\n\nThe `Option` type solves this by making the possibility of an absent value explicit in the type system. A function that returns `Option` cannot be mistaken for a function that returns `User`. The compiler forces you to handle the `None` case before you can access the value inside a `Some`, eliminating an entire class of bugs.\n\n---\n\n## Good Example\n\nA 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.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\n\nconst users: User[] = [\n { id: 1, name: \"Paul\" },\n { id: 2, name: \"Alex\" },\n];\n\n// This function safely returns an Option, not a User or null.\nconst findUserById = (id: number): Option.Option => {\n const user = users.find((u) => u.id === id);\n return Option.fromNullable(user); // A useful helper for existing APIs\n};\n\n// The caller MUST handle both cases.\nconst greeting = (id: number): string =>\n findUserById(id).pipe(\n Option.match({\n onNone: () => \"User not found.\",\n onSome: (user) => `Welcome, ${user.name}!`,\n })\n );\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(greeting(1)); // \"Welcome, Paul!\"\n yield* Effect.log(greeting(3)); // \"User not found.\"\n});\n\nEffect.runPromise(program);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is returning a nullable type (e.g., User | null or User | undefined). This relies on the discipline of every single caller to perform a null check. Forgetting even one check can introduce a runtime error.\n\n```typescript\ninterface User {\n id: number;\n name: string;\n}\nconst users: User[] = [{ id: 1, name: \"Paul\" }];\n\n// ❌ WRONG: This function's return type is less safe.\nconst findUserUnsafely = (id: number): User | undefined => {\n return users.find((u) => u.id === id);\n};\n\nconst user = findUserUnsafely(3);\n\n// This will throw \"TypeError: Cannot read properties of undefined (reading 'name')\"\n// because the caller forgot to check if the user exists.\nconsole.log(`User's name is ${user.name}`);\n```" - }, - { - "id": "data-option", - "title": "Model Optional Values Safely with Option", - "description": "Use Option to model values that may be present or absent, making absence explicit and type-safe.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Option } from \"effect\";\n\n// Create an Option from a value\nconst someValue = Option.some(42); // Option\nconst noValue = Option.none(); // Option\n\n// Safely convert a nullable value to Option\nconst fromNullable = Option.fromNullable(Math.random() > 0.5 ? \"hello\" : null); // Option\n\n// Pattern match on Option\nconst result = someValue.pipe(\n Option.match({\n onNone: () => \"No value\",\n onSome: (n) => `Value: ${n}`,\n })\n); // string\n\n// Use Option in a workflow\nfunction findUser(id: number): Option.Option<{ id: number; name: string }> {\n return id === 1 ? Option.some({ id, name: \"Alice\" }) : Option.none();\n}\n```\n\n**Explanation:**\n\n- `Option.some(value)` represents a present value.\n- `Option.none()` represents absence.\n- `Option.fromNullable` safely lifts nullable values into Option.\n- Pattern matching ensures all cases are handled.", - "antiPattern": "Using `null` or `undefined` to represent absence, or forgetting to handle the \"no value\" case, which leads to runtime errors and less maintainable code.", - "explanation": "`Option` makes it impossible to forget to handle the \"no value\" case. \nIt improves code safety, readability, and composability, and is a foundation for robust domain modeling.", - "content": "# Model Optional Values Safely with `Option`\n\n## Guideline\n\nUse the `Option` data type to represent values that may or may not exist. \nThis eliminates the need for `null` or `undefined`, making absence explicit and type-safe.\n\n## Rationale\n\n`Option` makes it impossible to forget to handle the \"no value\" case. \nIt improves code safety, readability, and composability, and is a foundation for robust domain modeling.\n\n## Good Example\n\n```typescript\nimport { Option } from \"effect\";\n\n// Create an Option from a value\nconst someValue = Option.some(42); // Option\nconst noValue = Option.none(); // Option\n\n// Safely convert a nullable value to Option\nconst fromNullable = Option.fromNullable(Math.random() > 0.5 ? \"hello\" : null); // Option\n\n// Pattern match on Option\nconst result = someValue.pipe(\n Option.match({\n onNone: () => \"No value\",\n onSome: (n) => `Value: ${n}`,\n })\n); // string\n\n// Use Option in a workflow\nfunction findUser(id: number): Option.Option<{ id: number; name: string }> {\n return id === 1 ? Option.some({ id, name: \"Alice\" }) : Option.none();\n}\n```\n\n**Explanation:**\n\n- `Option.some(value)` represents a present value.\n- `Option.none()` represents absence.\n- `Option.fromNullable` safely lifts nullable values into Option.\n- Pattern matching ensures all cases are handled.\n\n## Anti-Pattern\n\nUsing `null` or `undefined` to represent absence, or forgetting to handle the \"no value\" case, which leads to runtime errors and less maintainable code." - }, - { - "id": "model-validated-domain-types-with-brand", - "title": "Model Validated Domain Types with Brand", - "description": "Model validated domain types with Brand.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Brand, Option } from \"effect\";\n\ntype Email = string & Brand.Brand<\"Email\">;\n\nconst makeEmail = (s: string): Option.Option =>\n s.includes(\"@\") ? Option.some(s as Email) : Option.none();\n\n// A function can now trust that its input is a valid email.\nconst sendEmail = (email: Email, body: string) => {\n /* ... */\n};\n```\n\n**Explanation:** \nBranding ensures that only validated values are used, reducing bugs and\nrepetitive checks.", - "antiPattern": "\"Primitive obsession\"—using raw primitives (`string`, `number`) and performing\nvalidation inside every function that uses them. This is repetitive and\nerror-prone.", - "explanation": "This pattern moves validation to the boundaries of your system. Once a value\nhas been branded, the rest of your application can trust that it is valid,\neliminating repetitive checks.", - "content": "# Model Validated Domain Types with Brand\n\n## Guideline\n\nFor domain primitives that have specific rules (e.g., a valid email), create a\nBranded Type. This ensures a value can only be created after passing a\nvalidation check.\n\n## Rationale\n\nThis pattern moves validation to the boundaries of your system. Once a value\nhas been branded, the rest of your application can trust that it is valid,\neliminating repetitive checks.\n\n## Good Example\n\n```typescript\nimport { Brand, Option } from \"effect\";\n\ntype Email = string & Brand.Brand<\"Email\">;\n\nconst makeEmail = (s: string): Option.Option =>\n s.includes(\"@\") ? Option.some(s as Email) : Option.none();\n\n// A function can now trust that its input is a valid email.\nconst sendEmail = (email: Email, body: string) => {\n /* ... */\n};\n```\n\n**Explanation:** \nBranding ensures that only validated values are used, reducing bugs and\nrepetitive checks.\n\n## Anti-Pattern\n\n\"Primitive obsession\"—using raw primitives (`string`, `number`) and performing\nvalidation inside every function that uses them. This is repetitive and\nerror-prone." - }, - { - "id": "data-exit", - "title": "Modeling Effect Results with Exit", - "description": "Use Exit to capture the outcome of an Effect, including success, failure, and defects, for robust error handling and coordination.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Exit } from \"effect\";\n\n// Run an Effect and capture its Exit value\nconst program = Effect.succeed(42);\n\nconst runAndCapture = Effect.runPromiseExit(program); // Promise>\n\n// Pattern match on Exit\nrunAndCapture.then((exit) => {\n if (Exit.isSuccess(exit)) {\n console.log(\"Success:\", exit.value);\n } else if (Exit.isFailure(exit)) {\n console.error(\"Failure:\", exit.cause);\n }\n});\n```\n\n**Explanation:**\n\n- `Exit` captures both success (`Exit.success(value)`) and failure (`Exit.failure(cause)`).\n- Use `Exit` for robust error handling, supervision, and coordination of concurrent effects.\n- Pattern matching on `Exit` lets you handle all possible outcomes.", - "antiPattern": "Ignoring the outcome of an effect, or only handling success/failure without distinguishing between error types or defects, which can lead to missed errors and less robust code.", - "explanation": "When running or supervising effects, you often need to know not just if they succeeded or failed, but _how_ they failed (e.g., error vs. defect). \n`Exit` provides a complete, type-safe summary of an effect's outcome.", - "content": "# Modeling Effect Results with `Exit`\n\n## Guideline\n\nUse the `Exit` data type to represent the result of running an `Effect`, capturing both success and failure (including defects) in a type-safe way. \n`Exit` is especially useful for coordinating concurrent workflows and robust error handling.\n\n## Rationale\n\nWhen running or supervising effects, you often need to know not just if they succeeded or failed, but _how_ they failed (e.g., error vs. defect). \n`Exit` provides a complete, type-safe summary of an effect's outcome.\n\n## Good Example\n\n```typescript\nimport { Effect, Exit } from \"effect\";\n\n// Run an Effect and capture its Exit value\nconst program = Effect.succeed(42);\n\nconst runAndCapture = Effect.runPromiseExit(program); // Promise>\n\n// Pattern match on Exit\nrunAndCapture.then((exit) => {\n if (Exit.isSuccess(exit)) {\n console.log(\"Success:\", exit.value);\n } else if (Exit.isFailure(exit)) {\n console.error(\"Failure:\", exit.cause);\n }\n});\n```\n\n**Explanation:**\n\n- `Exit` captures both success (`Exit.success(value)`) and failure (`Exit.failure(cause)`).\n- Use `Exit` for robust error handling, supervision, and coordination of concurrent effects.\n- Pattern matching on `Exit` lets you handle all possible outcomes.\n\n## Anti-Pattern\n\nIgnoring the outcome of an effect, or only handling success/failure without distinguishing between error types or defects, which can lead to missed errors and less robust code." - }, - { - "id": "data-case", - "title": "Modeling Tagged Unions with Data.case", - "description": "Use Data.case to define tagged unions (ADTs) for modeling domain-specific states and enabling exhaustive pattern matching.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Data } from \"effect\";\n\n// Define a tagged union for a simple state machine\ntype State = Data.TaggedEnum<{\n Loading: {};\n Success: { data: string };\n Failure: { error: string };\n}>;\nconst { Loading, Success, Failure } = Data.taggedEnum();\n\n// Create instances\nconst state1: State = Loading();\nconst state2: State = Success({ data: \"Hello\" });\nconst state3: State = Failure({ error: \"Oops\" });\n\n// Pattern match on the state\nfunction handleState(state: State): string {\n switch (state._tag) {\n case \"Loading\":\n return \"Loading...\";\n case \"Success\":\n return `Data: ${state.data}`;\n case \"Failure\":\n return `Error: ${state.error}`;\n }\n}\n```\n\n**Explanation:**\n\n- `Data.case` creates tagged constructors for each state.\n- The `_tag` property enables exhaustive pattern matching.\n- Use for domain modeling, state machines, and error types.", - "antiPattern": "Using plain objects or enums for domain states, which can lead to illegal states, missed cases, and less type-safe pattern matching.", - "explanation": "Modeling domain logic with tagged unions ensures that all cases are handled, prevents illegal states, and enables safe, exhaustive pattern matching. \n`Data.case` provides a concise, type-safe way to define and use ADTs in your application.", - "content": "# Modeling Tagged Unions with `Data.case`\n\n## Guideline\n\nUse `Data.case` to create tagged unions (algebraic data types, or ADTs) for robust, type-safe domain modeling. \nTagged unions make it easy to represent and exhaustively handle all possible states of your domain entities.\n\n## Rationale\n\nModeling domain logic with tagged unions ensures that all cases are handled, prevents illegal states, and enables safe, exhaustive pattern matching. \n`Data.case` provides a concise, type-safe way to define and use ADTs in your application.\n\n## Good Example\n\n```typescript\nimport { Data } from \"effect\";\n\n// Define a tagged union for a simple state machine\ntype State = Data.TaggedEnum<{\n Loading: {};\n Success: { data: string };\n Failure: { error: string };\n}>;\nconst { Loading, Success, Failure } = Data.taggedEnum();\n\n// Create instances\nconst state1: State = Loading();\nconst state2: State = Success({ data: \"Hello\" });\nconst state3: State = Failure({ error: \"Oops\" });\n\n// Pattern match on the state\nfunction handleState(state: State): string {\n switch (state._tag) {\n case \"Loading\":\n return \"Loading...\";\n case \"Success\":\n return `Data: ${state.data}`;\n case \"Failure\":\n return `Error: ${state.error}`;\n }\n}\n```\n\n**Explanation:**\n\n- `Data.case` creates tagged constructors for each state.\n- The `_tag` property enables exhaustive pattern matching.\n- Use for domain modeling, state machines, and error types.\n\n## Anti-Pattern\n\nUsing plain objects or enums for domain states, which can lead to illegal states, missed cases, and less type-safe pattern matching." - }, - { - "id": "brand-model-domain-type", - "title": "Modeling Validated Domain Types with Brand", - "description": "Use Brand to define types like Email, UserId, or PositiveInt, ensuring only valid values can be constructed and used.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Brand } from \"effect\";\n\n// Define a branded type for Email\ntype Email = string & Brand.Brand<\"Email\">;\n\n// Function that only accepts Email, not any string\nfunction sendWelcome(email: Email) {\n // ...\n}\n\n// Constructing an Email value (unsafe, see next pattern for validation)\nconst email = \"user@example.com\" as Email;\n\nsendWelcome(email); // OK\n// sendWelcome(\"not-an-email\"); // Type error! (commented to allow compilation)\n```\n\n**Explanation:**\n\n- `Brand.Branded` creates a new type that is distinct from its base type.\n- Only values explicitly branded as `Email` can be used where an `Email` is required.\n- This prevents accidental mixing of domain types.", - "antiPattern": "Using plain strings or numbers for domain-specific values (like emails, user IDs, or currency codes), which can lead to accidental misuse and bugs that are hard to catch.", - "explanation": "Branded types add a layer of type safety, ensuring that values like `Email`, `UserId`, or `PositiveInt` are not confused with plain strings or numbers. \nThey help you catch bugs at compile time and make your code more self-documenting.", - "content": "# Modeling Validated Domain Types with `Brand`\n\n## Guideline\n\nUse the `Brand` utility to create domain-specific types from primitives like `string` or `number`. \nThis prevents accidental misuse and makes illegal states unrepresentable in your codebase.\n\n## Rationale\n\nBranded types add a layer of type safety, ensuring that values like `Email`, `UserId`, or `PositiveInt` are not confused with plain strings or numbers. \nThey help you catch bugs at compile time and make your code more self-documenting.\n\n## Good Example\n\n```typescript\nimport { Brand } from \"effect\";\n\n// Define a branded type for Email\ntype Email = string & Brand.Brand<\"Email\">;\n\n// Function that only accepts Email, not any string\nfunction sendWelcome(email: Email) {\n // ...\n}\n\n// Constructing an Email value (unsafe, see next pattern for validation)\nconst email = \"user@example.com\" as Email;\n\nsendWelcome(email); // OK\n// sendWelcome(\"not-an-email\"); // Type error! (commented to allow compilation)\n```\n\n**Explanation:**\n\n- `Brand.Branded` creates a new type that is distinct from its base type.\n- Only values explicitly branded as `Email` can be used where an `Email` is required.\n- This prevents accidental mixing of domain types.\n\n## Anti-Pattern\n\nUsing plain strings or numbers for domain-specific values (like emails, user IDs, or currency codes), which can lead to accidental misuse and bugs that are hard to catch." - }, - { - "id": "optional-pattern-handling-none-some", - "title": "Optional Pattern 1: Handling None and Some Values", - "description": "Use Option to represent values that may not exist, replacing null/undefined with type-safe Option that forces explicit handling.", - "skillLevel": "intermediate", - "useCases": [ - "value-handling" - ], - "example": "This example demonstrates Option handling patterns.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ninterface User {\n id: string;\n name: string;\n email: string;\n}\n\ninterface Profile {\n bio: string;\n website?: string;\n location?: string;\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[OPTION HANDLING] None/Some values and pattern matching\\n`\n );\n\n // Example 1: Creating Options\n console.log(`[1] Creating Option values:\\n`);\n\n const someValue: Option.Option = Option.some(\"data\");\n const noneValue: Option.Option = Option.none();\n\n const displayOption = (opt: Option.Option, label: string) =>\n Effect.gen(function* () {\n if (Option.isSome(opt)) {\n yield* Effect.log(`${label}: Some(${opt.value})`);\n } else {\n yield* Effect.log(`${label}: None`);\n }\n });\n\n yield* displayOption(someValue, \"someValue\");\n yield* displayOption(noneValue, \"noneValue\");\n\n // Example 2: Creating from nullable values\n console.log(`\\n[2] Converting nullable to Option:\\n`);\n\n const possiblyNull = (shouldExist: boolean): string | null =>\n shouldExist ? \"found\" : null;\n\n const toOption = (value: string | null | undefined): Option.Option =>\n value ? Option.some(value) : Option.none();\n\n const opt1 = toOption(possiblyNull(true));\n const opt2 = toOption(possiblyNull(false));\n\n yield* displayOption(opt1, \"toOption(found)\");\n yield* displayOption(opt2, \"toOption(null)\");\n\n // Example 3: Pattern matching on Option\n console.log(`\\n[3] Pattern matching with match():\\n`);\n\n const userId: Option.Option = Option.some(\"user-123\");\n\n const message = Option.match(userId, {\n onSome: (id) => `User ID: ${id}`,\n onNone: () => \"No user found\",\n });\n\n yield* Effect.log(`[MATCH] ${message}`);\n\n const emptyUserId: Option.Option = Option.none();\n\n const emptyMessage = Option.match(emptyUserId, {\n onSome: (id) => `User ID: ${id}`,\n onNone: () => \"No user found\",\n });\n\n yield* Effect.log(`[MATCH] ${emptyMessage}\\n`);\n\n // Example 4: Transforming with map\n console.log(`[4] Transforming values with map():\\n`);\n\n const userCount: Option.Option = Option.some(42);\n\n const doubled = Option.map(userCount, (count) => count * 2);\n\n yield* displayOption(doubled, \"doubled\");\n\n // Chaining maps\n const email: Option.Option = Option.some(\"user@example.com\");\n\n const domain = Option.map(email, (e) =>\n e.split(\"@\")[1] ?? \"unknown\"\n );\n\n yield* displayOption(domain, \"email domain\");\n\n // Example 5: Chaining with flatMap\n console.log(`\\n[5] Chaining operations with flatMap():\\n`);\n\n const findUser = (id: string): Option.Option =>\n id === \"user-1\"\n ? Option.some({ id, name: \"Alice\", email: \"alice@example.com\" })\n : Option.none();\n\n const getProfile = (userId: string): Option.Option =>\n userId === \"user-1\"\n ? Option.some({ bio: \"Developer\", website: \"alice.dev\" })\n : Option.none();\n\n const userId2 = Option.some(\"user-1\");\n\n // Chained operations: userId -> user -> profile\n const profileChain = Option.flatMap(userId2, (id) =>\n Option.flatMap(findUser(id), (user) =>\n getProfile(user.id)\n )\n );\n\n const profileResult = Option.match(profileChain, {\n onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,\n onNone: () => \"No profile found\",\n });\n\n yield* Effect.log(`[CHAIN] ${profileResult}\\n`);\n\n // Example 6: Fallback values with getOrElse\n console.log(`[6] Default values with getOrElse():\\n`);\n\n const optionalStatus: Option.Option = Option.none();\n\n const status = Option.getOrElse(optionalStatus, () => \"unknown\");\n\n yield* Effect.log(`[DEFAULT] Status: ${status}`);\n\n // Real value\n const knownStatus: Option.Option = Option.some(\"active\");\n\n const realStatus = Option.getOrElse(knownStatus, () => \"unknown\");\n\n yield* Effect.log(`[VALUE] Status: ${realStatus}\\n`);\n\n // Example 7: Filter with predicate\n console.log(`[7] Filtering with conditions:\\n`);\n\n const ageOption: Option.Option = Option.some(25);\n\n const isAdult = Option.filter(ageOption, (age) => age >= 18);\n\n yield* displayOption(isAdult, \"Adult check (25)\");\n\n const ageOption2: Option.Option = Option.some(15);\n\n const isAdult2 = Option.filter(ageOption2, (age) => age >= 18);\n\n yield* displayOption(isAdult2, \"Adult check (15)\");\n\n // Example 8: Multiple Options (all present?)\n console.log(`\\n[8] Combining multiple Options:\\n`);\n\n const firstName: Option.Option = Option.some(\"John\");\n const lastName: Option.Option = Option.some(\"Doe\");\n const middleName: Option.Option = Option.none();\n\n // All three present?\n const allPresent = Option.all([firstName, lastName, middleName]);\n\n yield* displayOption(allPresent, \"All present\");\n\n // Just two\n const twoPresent = Option.all([firstName, lastName]);\n\n yield* displayOption(twoPresent, \"Two present\");\n\n // Example 9: Converting Option to Error\n console.log(`\\n[9] Converting Option to Result/Error:\\n`);\n\n const optionalConfig: Option.Option<{ apiKey: string }> = Option.none();\n\n const configOrError = Option.match(optionalConfig, {\n onSome: (config) => config,\n onNone: () => {\n throw new Error(\"Configuration not found\");\n },\n });\n\n // In real code, would catch error\n const result = Option.match(optionalConfig, {\n onSome: (config) => ({ success: true, value: config }),\n onNone: () => ({ success: false, error: \"config-not-found\" }),\n });\n\n yield* Effect.log(`[CONVERT] ${JSON.stringify(result)}\\n`);\n\n // Example 10: Option in business logic\n console.log(`[10] Practical: Optional user settings:\\n`);\n\n const userSettings: Option.Option<{\n theme: string;\n notifications: boolean;\n }> = Option.some({\n theme: \"dark\",\n notifications: true,\n });\n\n const getTheme = Option.map(userSettings, (s) => s.theme);\n const theme = Option.getOrElse(getTheme, () => \"light\"); // Default\n\n yield* Effect.log(`[SETTING] Theme: ${theme}`);\n\n // No settings\n const noSettings: Option.Option<{ theme: string; notifications: boolean }> =\n Option.none();\n\n const noTheme = Option.map(noSettings, (s) => s.theme);\n const defaultTheme = Option.getOrElse(noTheme, () => \"light\");\n\n yield* Effect.log(`[DEFAULT] Theme: ${defaultTheme}`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Null/undefined causes widespread bugs:\n\n**Problem 1: Billion-dollar mistake**\n- Tony Hoare invented null in ALGOL in 1965\n- Created \"billion-dollar mistake\"\n- 90% of security vulnerabilities involve null handling\n\n**Problem 2: Undefined behavior**\n- `user.profile.name` - any property could be null\n- Runtime error: \"Cannot read property 'name' of undefined\"\n- No compile-time warning\n- Production crash\n\n**Problem 3: Silent failures**\n- Function returns null on failure\n- Caller doesn't check\n- Uses null as if it's a value\n- Corrupts state downstream\n\n**Problem 4: Conditional hell**\n```javascript\nif (user !== null && user.profile !== null && user.profile.name !== null) {\n // Do thing\n}\n```\n\nSolutions:\n\n**Option type**:\n- `Some(value)` = value exists\n- `None` = value doesn't exist\n- Type system forces checking\n- No silent null checks possible\n\n**Pattern matching**:\n- `Option.match()`\n- Handle both cases explicitly\n- Compiler warns if you miss one\n\n**Chaining**:\n- `option.map().flatMap().match()`\n- Pipeline of operations\n- Null-safe by design\n\n---", - "content": "## Guideline\n\nOption enables null-safe programming:\n\n- **Some(value)**: Value exists\n- **None**: Value doesn't exist\n- **Pattern matching**: Handle both cases\n- **Chaining**: Compose operations safely\n- **Fallbacks**: Default values\n- **Conversions**: Option ↔ Error\n\nPattern: Use `Option.isSome()`, `Option.isNone()`, `match()`, `map()`, `flatMap()`\n\n---\n\n## Rationale\n\nNull/undefined causes widespread bugs:\n\n**Problem 1: Billion-dollar mistake**\n- Tony Hoare invented null in ALGOL in 1965\n- Created \"billion-dollar mistake\"\n- 90% of security vulnerabilities involve null handling\n\n**Problem 2: Undefined behavior**\n- `user.profile.name` - any property could be null\n- Runtime error: \"Cannot read property 'name' of undefined\"\n- No compile-time warning\n- Production crash\n\n**Problem 3: Silent failures**\n- Function returns null on failure\n- Caller doesn't check\n- Uses null as if it's a value\n- Corrupts state downstream\n\n**Problem 4: Conditional hell**\n```javascript\nif (user !== null && user.profile !== null && user.profile.name !== null) {\n // Do thing\n}\n```\n\nSolutions:\n\n**Option type**:\n- `Some(value)` = value exists\n- `None` = value doesn't exist\n- Type system forces checking\n- No silent null checks possible\n\n**Pattern matching**:\n- `Option.match()`\n- Handle both cases explicitly\n- Compiler warns if you miss one\n\n**Chaining**:\n- `option.map().flatMap().match()`\n- Pipeline of operations\n- Null-safe by design\n\n---\n\n## Good Example\n\nThis example demonstrates Option handling patterns.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ninterface User {\n id: string;\n name: string;\n email: string;\n}\n\ninterface Profile {\n bio: string;\n website?: string;\n location?: string;\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[OPTION HANDLING] None/Some values and pattern matching\\n`\n );\n\n // Example 1: Creating Options\n console.log(`[1] Creating Option values:\\n`);\n\n const someValue: Option.Option = Option.some(\"data\");\n const noneValue: Option.Option = Option.none();\n\n const displayOption = (opt: Option.Option, label: string) =>\n Effect.gen(function* () {\n if (Option.isSome(opt)) {\n yield* Effect.log(`${label}: Some(${opt.value})`);\n } else {\n yield* Effect.log(`${label}: None`);\n }\n });\n\n yield* displayOption(someValue, \"someValue\");\n yield* displayOption(noneValue, \"noneValue\");\n\n // Example 2: Creating from nullable values\n console.log(`\\n[2] Converting nullable to Option:\\n`);\n\n const possiblyNull = (shouldExist: boolean): string | null =>\n shouldExist ? \"found\" : null;\n\n const toOption = (value: string | null | undefined): Option.Option =>\n value ? Option.some(value) : Option.none();\n\n const opt1 = toOption(possiblyNull(true));\n const opt2 = toOption(possiblyNull(false));\n\n yield* displayOption(opt1, \"toOption(found)\");\n yield* displayOption(opt2, \"toOption(null)\");\n\n // Example 3: Pattern matching on Option\n console.log(`\\n[3] Pattern matching with match():\\n`);\n\n const userId: Option.Option = Option.some(\"user-123\");\n\n const message = Option.match(userId, {\n onSome: (id) => `User ID: ${id}`,\n onNone: () => \"No user found\",\n });\n\n yield* Effect.log(`[MATCH] ${message}`);\n\n const emptyUserId: Option.Option = Option.none();\n\n const emptyMessage = Option.match(emptyUserId, {\n onSome: (id) => `User ID: ${id}`,\n onNone: () => \"No user found\",\n });\n\n yield* Effect.log(`[MATCH] ${emptyMessage}\\n`);\n\n // Example 4: Transforming with map\n console.log(`[4] Transforming values with map():\\n`);\n\n const userCount: Option.Option = Option.some(42);\n\n const doubled = Option.map(userCount, (count) => count * 2);\n\n yield* displayOption(doubled, \"doubled\");\n\n // Chaining maps\n const email: Option.Option = Option.some(\"user@example.com\");\n\n const domain = Option.map(email, (e) =>\n e.split(\"@\")[1] ?? \"unknown\"\n );\n\n yield* displayOption(domain, \"email domain\");\n\n // Example 5: Chaining with flatMap\n console.log(`\\n[5] Chaining operations with flatMap():\\n`);\n\n const findUser = (id: string): Option.Option =>\n id === \"user-1\"\n ? Option.some({ id, name: \"Alice\", email: \"alice@example.com\" })\n : Option.none();\n\n const getProfile = (userId: string): Option.Option =>\n userId === \"user-1\"\n ? Option.some({ bio: \"Developer\", website: \"alice.dev\" })\n : Option.none();\n\n const userId2 = Option.some(\"user-1\");\n\n // Chained operations: userId -> user -> profile\n const profileChain = Option.flatMap(userId2, (id) =>\n Option.flatMap(findUser(id), (user) =>\n getProfile(user.id)\n )\n );\n\n const profileResult = Option.match(profileChain, {\n onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,\n onNone: () => \"No profile found\",\n });\n\n yield* Effect.log(`[CHAIN] ${profileResult}\\n`);\n\n // Example 6: Fallback values with getOrElse\n console.log(`[6] Default values with getOrElse():\\n`);\n\n const optionalStatus: Option.Option = Option.none();\n\n const status = Option.getOrElse(optionalStatus, () => \"unknown\");\n\n yield* Effect.log(`[DEFAULT] Status: ${status}`);\n\n // Real value\n const knownStatus: Option.Option = Option.some(\"active\");\n\n const realStatus = Option.getOrElse(knownStatus, () => \"unknown\");\n\n yield* Effect.log(`[VALUE] Status: ${realStatus}\\n`);\n\n // Example 7: Filter with predicate\n console.log(`[7] Filtering with conditions:\\n`);\n\n const ageOption: Option.Option = Option.some(25);\n\n const isAdult = Option.filter(ageOption, (age) => age >= 18);\n\n yield* displayOption(isAdult, \"Adult check (25)\");\n\n const ageOption2: Option.Option = Option.some(15);\n\n const isAdult2 = Option.filter(ageOption2, (age) => age >= 18);\n\n yield* displayOption(isAdult2, \"Adult check (15)\");\n\n // Example 8: Multiple Options (all present?)\n console.log(`\\n[8] Combining multiple Options:\\n`);\n\n const firstName: Option.Option = Option.some(\"John\");\n const lastName: Option.Option = Option.some(\"Doe\");\n const middleName: Option.Option = Option.none();\n\n // All three present?\n const allPresent = Option.all([firstName, lastName, middleName]);\n\n yield* displayOption(allPresent, \"All present\");\n\n // Just two\n const twoPresent = Option.all([firstName, lastName]);\n\n yield* displayOption(twoPresent, \"Two present\");\n\n // Example 9: Converting Option to Error\n console.log(`\\n[9] Converting Option to Result/Error:\\n`);\n\n const optionalConfig: Option.Option<{ apiKey: string }> = Option.none();\n\n const configOrError = Option.match(optionalConfig, {\n onSome: (config) => config,\n onNone: () => {\n throw new Error(\"Configuration not found\");\n },\n });\n\n // In real code, would catch error\n const result = Option.match(optionalConfig, {\n onSome: (config) => ({ success: true, value: config }),\n onNone: () => ({ success: false, error: \"config-not-found\" }),\n });\n\n yield* Effect.log(`[CONVERT] ${JSON.stringify(result)}\\n`);\n\n // Example 10: Option in business logic\n console.log(`[10] Practical: Optional user settings:\\n`);\n\n const userSettings: Option.Option<{\n theme: string;\n notifications: boolean;\n }> = Option.some({\n theme: \"dark\",\n notifications: true,\n });\n\n const getTheme = Option.map(userSettings, (s) => s.theme);\n const theme = Option.getOrElse(getTheme, () => \"light\"); // Default\n\n yield* Effect.log(`[SETTING] Theme: ${theme}`);\n\n // No settings\n const noSettings: Option.Option<{ theme: string; notifications: boolean }> =\n Option.none();\n\n const noTheme = Option.map(noSettings, (s) => s.theme);\n const defaultTheme = Option.getOrElse(noTheme, () => \"light\");\n\n yield* Effect.log(`[DEFAULT] Theme: ${defaultTheme}`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Option Chains\n\nBuild complex pipelines with Options:\n\n```typescript\nconst processUser = (\n userId: Option.Option\n): Option.Option<{ email: string; verified: boolean }> =>\n Option.flatMap(userId, (id) =>\n Option.flatMap(findUser(id), (user) =>\n Option.flatMap(loadSettings(user.id), (settings) =>\n Option.some({\n email: user.email,\n verified: settings.emailVerified,\n })\n )\n )\n );\n\n// Or using pipe syntax\nconst processUserPipe = (userId: Option.Option) =>\n userId\n .pipe(Option.flatMap((id) => findUser(id)))\n .pipe(Option.flatMap((user) => loadSettings(user.id)))\n .pipe(Option.map((settings) => ({ verified: settings.emailVerified })));\n```\n\n---\n\n## Advanced: Lifting Functions\n\nConvert regular functions to work with Options:\n\n```typescript\nconst liftOption = (\n f: (a: A) => B\n): ((opt: Option.Option) => Option.Option) =>\n (opt: Option.Option) =>\n Option.isSome(opt) ? Option.some(f(opt.value)) : Option.none();\n\n// Usage\nconst parseNumber = liftOption((s: string) => parseInt(s));\n\nconst result1 = parseNumber(Option.some(\"42\")); // Some(42)\nconst result2 = parseNumber(Option.none()); // None\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Option when:**\n- Value may not exist\n- Nullable database fields\n- Optional function parameters\n- Dictionary lookups\n- Parsing/validation\n\n⚠️ **Trade-offs:**\n- More code than null checks\n- Requires pattern matching\n- Learning curve for teams\n- Some boilerplate\n\n---\n\n## Comparison: Null vs Option\n\n| Aspect | Null | Option |\n| --- | --- | --- |\n| **Safety** | Unsafe (runtime errors) | Safe (compile-time) |\n| **Verbosity** | Less code initially | More explicit |\n| **Bugs** | Easy to miss null | Forced to handle |\n| **Performance** | Slightly faster | Negligible difference |\n| **Debugging** | Hard (type: null) | Clear (Some/None) |\n\n---\n\n## See Also\n\n- [Optional Pattern 2: Optional Chains](./optional-pattern-optional-chains.mdx) - Advanced chaining\n- [Error Handling Pattern 3: Custom Strategies](./error-handling-pattern-custom-strategies.mdx) - Error types\n- [Stream Pattern 1: Map & Filter](./stream-pattern-map-filter-transformations.mdx) - Stream transformations\n- [State Management Pattern 1: SynchronizedRef](./state-management-pattern-synchronized-ref.mdx) - State safety" - }, - { - "id": "optional-pattern-optional-chains", - "title": "Optional Pattern 2: Optional Chaining and Composition", - "description": "Use Option combinators (map, flatMap, ap) to compose operations that may fail, creating readable and maintainable pipelines.", - "skillLevel": "advanced", - "useCases": [ - "value-handling" - ], - "example": "This example demonstrates optional chaining patterns.\n\n```typescript\nimport { Effect, Option, pipe } from \"effect\";\n\ninterface User {\n id: string;\n name: string;\n email: string;\n}\n\ninterface Profile {\n bio: string;\n website?: string;\n avatar?: string;\n}\n\ninterface Settings {\n theme: \"light\" | \"dark\";\n notifications: boolean;\n language: string;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[OPTIONAL CHAINING] Composing Option operations\\n`);\n\n // Example 1: Simple chain with map\n console.log(`[1] Chaining transformations with map():\\n`);\n\n const userId: Option.Option = Option.some(\"user-42\");\n\n const userDisplayId = Option.map(userId, (id) => `User#${id}`);\n\n const idMessage = Option.match(userDisplayId, {\n onSome: (display) => display,\n onNone: () => \"No user ID\",\n });\n\n yield* Effect.log(`[CHAIN 1] ${idMessage}`);\n\n // Chained maps\n const email: Option.Option = Option.some(\"alice@example.com\");\n\n const emailParts = pipe(\n email,\n Option.map((e) => e.toLowerCase()),\n Option.map((e) => e.split(\"@\")),\n Option.map((parts) => parts[0]) // username\n );\n\n const username = Option.getOrElse(emailParts, () => \"unknown\");\n\n yield* Effect.log(`[USERNAME] ${username}\\n`);\n\n // Example 2: FlatMap for chaining operations that return Option\n console.log(`[2] Chaining operations with flatMap():\\n`);\n\n const findUser = (id: string): Option.Option =>\n id === \"user-42\"\n ? Option.some({\n id,\n name: \"Alice\",\n email: \"alice@example.com\",\n })\n : Option.none();\n\n const getProfile = (userId: string): Option.Option =>\n userId === \"user-42\"\n ? Option.some({\n bio: \"Software engineer\",\n website: \"alice.dev\",\n avatar: \"https://example.com/avatar.jpg\",\n })\n : Option.none();\n\n const userProfile = pipe(\n Option.some(\"user-42\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getProfile(user.id))\n );\n\n const profileInfo = Option.match(userProfile, {\n onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,\n onNone: () => \"Profile not found\",\n });\n\n yield* Effect.log(`[PROFILE] ${profileInfo}\\n`);\n\n // Example 3: Complex pipeline\n console.log(`[3] Complex pipeline (user → profile → settings → theme):\\n`);\n\n const getSettings = (userId: string): Option.Option =>\n userId === \"user-42\"\n ? Option.some({\n theme: \"dark\",\n notifications: true,\n language: \"en\",\n })\n : Option.none();\n\n const userTheme = pipe(\n Option.some(\"user-42\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getSettings(user.id)),\n Option.map((settings) => settings.theme)\n );\n\n const theme = Option.getOrElse(userTheme, () => \"light\");\n\n yield* Effect.log(`[THEME] ${theme}`);\n\n // Even if any step is None, result is None\n const invalidUserTheme = pipe(\n Option.some(\"invalid-user\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getSettings(user.id)),\n Option.map((settings) => settings.theme)\n );\n\n const invalidTheme = Option.getOrElse(invalidUserTheme, () => \"light\");\n\n yield* Effect.log(`[DEFAULT THEME] ${invalidTheme}\\n`);\n\n // Example 4: Apply (ap) for combining independent Options\n console.log(`[4] Combining values with ap():\\n`);\n\n const firstName: Option.Option = Option.some(\"John\");\n const lastName: Option.Option = Option.some(\"Doe\");\n\n // Create a function wrapped in Option\n const combineNames = (first: string) => (last: string) =>\n `${first} ${last}`;\n\n const fullName = pipe(\n Option.some(combineNames),\n Option.ap(firstName),\n Option.ap(lastName)\n );\n\n const name = Option.getOrElse(fullName, () => \"Unknown\");\n\n yield* Effect.log(`[COMBINED] ${name}`);\n\n // If any is None\n const noLastName: Option.Option = Option.none();\n\n const incompleteName = pipe(\n Option.some(combineNames),\n Option.ap(firstName),\n Option.ap(noLastName)\n );\n\n const incompleteFull = Option.getOrElse(incompleteName, () => \"Incomplete\");\n\n yield* Effect.log(`[INCOMPLETE] ${incompleteFull}\\n`);\n\n // Example 5: Traverse for mapping over collections\n console.log(`[5] Working with collections (traverse):\\n`);\n\n const userIds: string[] = [\"user-42\", \"user-99\", \"user-1\"];\n\n // Try to load all users\n const allUsers = Option.all(\n userIds.map((id) => findUser(id))\n );\n\n const usersMessage = Option.match(allUsers, {\n onSome: (users) => `Loaded ${users.length} users`,\n onNone: () => \"Some users not found\",\n });\n\n yield* Effect.log(`[TRAVERSE] ${usersMessage}\\n`);\n\n // Example 6: Or/recovery with multiple options\n console.log(`[6] Fallback chains with orElse():\\n`);\n\n const getPrimaryEmail = (): Option.Option => Option.none();\n const getSecondaryEmail = (): Option.Option =>\n Option.some(\"backup@example.com\");\n const getTertiaryEmail = (): Option.Option =>\n Option.some(\"tertiary@example.com\");\n\n const email1 = pipe(\n getPrimaryEmail(),\n Option.orElse(() => getSecondaryEmail()),\n Option.orElse(() => getTertiaryEmail())\n );\n\n const contactEmail = Option.getOrElse(email1, () => \"no-email@example.com\");\n\n yield* Effect.log(`[FALLBACK] Using email: ${contactEmail}\\n`);\n\n // Example 7: Filtering options\n console.log(`[7] Filtering with predicates:\\n`);\n\n const age: Option.Option = Option.some(25);\n\n const canVote = pipe(\n age,\n Option.filter((a) => a >= 18)\n );\n\n const voteStatus = Option.match(canVote, {\n onSome: () => \"Can vote\",\n onNone: () => \"Too young to vote\",\n });\n\n yield* Effect.log(`[FILTER] ${voteStatus}`);\n\n // Multiple filters in chain\n const score: Option.Option = Option.some(85);\n\n const isAGrade = pipe(\n score,\n Option.filter((s) => s >= 80),\n Option.filter((s) => s < 90)\n );\n\n const grade = Option.match(isAGrade, {\n onSome: () => \"Grade A\",\n onNone: () => \"Not in A range\",\n });\n\n yield* Effect.log(`[GRADES] ${grade}\\n`);\n\n // Example 8: Practical: Database query chain\n console.log(`[8] Real-world: Database record chain:\\n`);\n\n const getRecord = (id: string): Option.Option<{ data: string; nested: { value: number } }> =>\n id === \"rec-1\"\n ? Option.some({\n data: \"content\",\n nested: { value: 42 },\n })\n : Option.none();\n\n const recordValue = pipe(\n Option.some(\"rec-1\"),\n Option.flatMap((id) => getRecord(id)),\n Option.map((rec) => rec.nested),\n Option.map((nested) => nested.value),\n Option.map((value) => value * 2)\n );\n\n const finalValue = Option.getOrElse(recordValue, () => 0);\n\n yield* Effect.log(`[VALUE] ${finalValue}`);\n\n // Missing record\n const missingValue = pipe(\n Option.some(\"rec-999\"),\n Option.flatMap((id) => getRecord(id)),\n Option.map((rec) => rec.nested),\n Option.map((nested) => nested.value),\n Option.map((value) => value * 2)\n );\n\n const defaultValue = Option.getOrElse(missingValue, () => 0);\n\n yield* Effect.log(`[DEFAULT] ${defaultValue}\\n`);\n\n // Example 9: Conditional chaining\n console.log(`[9] Conditional paths:\\n`);\n\n const loadUserWithFallback = (id: string) =>\n pipe(\n findUser(id),\n Option.flatMap((user) =>\n // Only get premium features if user exists\n user.name.includes(\"Alice\")\n ? Option.some({ ...user, isPremium: true })\n : Option.none()\n ),\n Option.orElse(() =>\n // Fallback: return basic user\n findUser(id)\n )\n );\n\n const result1 = loadUserWithFallback(\"user-42\");\n const result2 = loadUserWithFallback(\"user-99\");\n\n yield* Effect.log(\n `[CONDITIONAL 1] ${Option.match(result1, { onSome: (u) => `${u.name}`, onNone: () => \"Not found\" })}`\n );\n\n yield* Effect.log(\n `[CONDITIONAL 2] ${Option.match(result2, { onSome: (u) => `${u.name}`, onNone: () => \"Not found\" })}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Nested option handling becomes complex:\n\n**Problem 1: Pyramid of doom**\n```typescript\nif (user !== null) {\n if (user.profile !== null) {\n if (user.profile.preferences !== null) {\n if (user.profile.preferences.theme !== null) {\n // Finally do thing\n }\n }\n }\n}\n```\n\n**Problem 2: Repeated null checks**\n- Every step needs its own check\n- Code duplicates\n- Hard to refactor\n- Bugs easy to introduce\n\n**Problem 3: Logic scattered**\n- Transformation logic mixed with null checks\n- Hard to understand intent\n- Error-prone\n\nSolutions:\n\n**Option chaining**:\n- `None` flows through automatically\n- Transform only if `Some`\n- No intermediate checks needed\n\n**Composition**:\n- Combine functions cleanly\n- Separate concerns\n- Reusable pieces\n\n**Fallbacks**:\n- `orElse()` for recovery\n- Chain multiple alternatives\n- Graceful degradation\n\n---", - "content": "## Guideline\n\nOption chaining enables elegant data flows:\n\n- **map**: Transform value if present\n- **flatMap**: Chain operations that return Option\n- **ap**: Apply functions wrapped in Option\n- **traverse**: Map over collections with Option\n- **composition**: Combine multiple chains\n- **recovery**: Provide fallbacks\n\nPattern: Use `Option.map()`, `flatMap()`, `ap()`, pipe operators\n\n---\n\n## Rationale\n\nNested option handling becomes complex:\n\n**Problem 1: Pyramid of doom**\n```typescript\nif (user !== null) {\n if (user.profile !== null) {\n if (user.profile.preferences !== null) {\n if (user.profile.preferences.theme !== null) {\n // Finally do thing\n }\n }\n }\n}\n```\n\n**Problem 2: Repeated null checks**\n- Every step needs its own check\n- Code duplicates\n- Hard to refactor\n- Bugs easy to introduce\n\n**Problem 3: Logic scattered**\n- Transformation logic mixed with null checks\n- Hard to understand intent\n- Error-prone\n\nSolutions:\n\n**Option chaining**:\n- `None` flows through automatically\n- Transform only if `Some`\n- No intermediate checks needed\n\n**Composition**:\n- Combine functions cleanly\n- Separate concerns\n- Reusable pieces\n\n**Fallbacks**:\n- `orElse()` for recovery\n- Chain multiple alternatives\n- Graceful degradation\n\n---\n\n## Good Example\n\nThis example demonstrates optional chaining patterns.\n\n```typescript\nimport { Effect, Option, pipe } from \"effect\";\n\ninterface User {\n id: string;\n name: string;\n email: string;\n}\n\ninterface Profile {\n bio: string;\n website?: string;\n avatar?: string;\n}\n\ninterface Settings {\n theme: \"light\" | \"dark\";\n notifications: boolean;\n language: string;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[OPTIONAL CHAINING] Composing Option operations\\n`);\n\n // Example 1: Simple chain with map\n console.log(`[1] Chaining transformations with map():\\n`);\n\n const userId: Option.Option = Option.some(\"user-42\");\n\n const userDisplayId = Option.map(userId, (id) => `User#${id}`);\n\n const idMessage = Option.match(userDisplayId, {\n onSome: (display) => display,\n onNone: () => \"No user ID\",\n });\n\n yield* Effect.log(`[CHAIN 1] ${idMessage}`);\n\n // Chained maps\n const email: Option.Option = Option.some(\"alice@example.com\");\n\n const emailParts = pipe(\n email,\n Option.map((e) => e.toLowerCase()),\n Option.map((e) => e.split(\"@\")),\n Option.map((parts) => parts[0]) // username\n );\n\n const username = Option.getOrElse(emailParts, () => \"unknown\");\n\n yield* Effect.log(`[USERNAME] ${username}\\n`);\n\n // Example 2: FlatMap for chaining operations that return Option\n console.log(`[2] Chaining operations with flatMap():\\n`);\n\n const findUser = (id: string): Option.Option =>\n id === \"user-42\"\n ? Option.some({\n id,\n name: \"Alice\",\n email: \"alice@example.com\",\n })\n : Option.none();\n\n const getProfile = (userId: string): Option.Option =>\n userId === \"user-42\"\n ? Option.some({\n bio: \"Software engineer\",\n website: \"alice.dev\",\n avatar: \"https://example.com/avatar.jpg\",\n })\n : Option.none();\n\n const userProfile = pipe(\n Option.some(\"user-42\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getProfile(user.id))\n );\n\n const profileInfo = Option.match(userProfile, {\n onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,\n onNone: () => \"Profile not found\",\n });\n\n yield* Effect.log(`[PROFILE] ${profileInfo}\\n`);\n\n // Example 3: Complex pipeline\n console.log(`[3] Complex pipeline (user → profile → settings → theme):\\n`);\n\n const getSettings = (userId: string): Option.Option =>\n userId === \"user-42\"\n ? Option.some({\n theme: \"dark\",\n notifications: true,\n language: \"en\",\n })\n : Option.none();\n\n const userTheme = pipe(\n Option.some(\"user-42\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getSettings(user.id)),\n Option.map((settings) => settings.theme)\n );\n\n const theme = Option.getOrElse(userTheme, () => \"light\");\n\n yield* Effect.log(`[THEME] ${theme}`);\n\n // Even if any step is None, result is None\n const invalidUserTheme = pipe(\n Option.some(\"invalid-user\"),\n Option.flatMap((id) => findUser(id)),\n Option.flatMap((user) => getSettings(user.id)),\n Option.map((settings) => settings.theme)\n );\n\n const invalidTheme = Option.getOrElse(invalidUserTheme, () => \"light\");\n\n yield* Effect.log(`[DEFAULT THEME] ${invalidTheme}\\n`);\n\n // Example 4: Apply (ap) for combining independent Options\n console.log(`[4] Combining values with ap():\\n`);\n\n const firstName: Option.Option = Option.some(\"John\");\n const lastName: Option.Option = Option.some(\"Doe\");\n\n // Create a function wrapped in Option\n const combineNames = (first: string) => (last: string) =>\n `${first} ${last}`;\n\n const fullName = pipe(\n Option.some(combineNames),\n Option.ap(firstName),\n Option.ap(lastName)\n );\n\n const name = Option.getOrElse(fullName, () => \"Unknown\");\n\n yield* Effect.log(`[COMBINED] ${name}`);\n\n // If any is None\n const noLastName: Option.Option = Option.none();\n\n const incompleteName = pipe(\n Option.some(combineNames),\n Option.ap(firstName),\n Option.ap(noLastName)\n );\n\n const incompleteFull = Option.getOrElse(incompleteName, () => \"Incomplete\");\n\n yield* Effect.log(`[INCOMPLETE] ${incompleteFull}\\n`);\n\n // Example 5: Traverse for mapping over collections\n console.log(`[5] Working with collections (traverse):\\n`);\n\n const userIds: string[] = [\"user-42\", \"user-99\", \"user-1\"];\n\n // Try to load all users\n const allUsers = Option.all(\n userIds.map((id) => findUser(id))\n );\n\n const usersMessage = Option.match(allUsers, {\n onSome: (users) => `Loaded ${users.length} users`,\n onNone: () => \"Some users not found\",\n });\n\n yield* Effect.log(`[TRAVERSE] ${usersMessage}\\n`);\n\n // Example 6: Or/recovery with multiple options\n console.log(`[6] Fallback chains with orElse():\\n`);\n\n const getPrimaryEmail = (): Option.Option => Option.none();\n const getSecondaryEmail = (): Option.Option =>\n Option.some(\"backup@example.com\");\n const getTertiaryEmail = (): Option.Option =>\n Option.some(\"tertiary@example.com\");\n\n const email1 = pipe(\n getPrimaryEmail(),\n Option.orElse(() => getSecondaryEmail()),\n Option.orElse(() => getTertiaryEmail())\n );\n\n const contactEmail = Option.getOrElse(email1, () => \"no-email@example.com\");\n\n yield* Effect.log(`[FALLBACK] Using email: ${contactEmail}\\n`);\n\n // Example 7: Filtering options\n console.log(`[7] Filtering with predicates:\\n`);\n\n const age: Option.Option = Option.some(25);\n\n const canVote = pipe(\n age,\n Option.filter((a) => a >= 18)\n );\n\n const voteStatus = Option.match(canVote, {\n onSome: () => \"Can vote\",\n onNone: () => \"Too young to vote\",\n });\n\n yield* Effect.log(`[FILTER] ${voteStatus}`);\n\n // Multiple filters in chain\n const score: Option.Option = Option.some(85);\n\n const isAGrade = pipe(\n score,\n Option.filter((s) => s >= 80),\n Option.filter((s) => s < 90)\n );\n\n const grade = Option.match(isAGrade, {\n onSome: () => \"Grade A\",\n onNone: () => \"Not in A range\",\n });\n\n yield* Effect.log(`[GRADES] ${grade}\\n`);\n\n // Example 8: Practical: Database query chain\n console.log(`[8] Real-world: Database record chain:\\n`);\n\n const getRecord = (id: string): Option.Option<{ data: string; nested: { value: number } }> =>\n id === \"rec-1\"\n ? Option.some({\n data: \"content\",\n nested: { value: 42 },\n })\n : Option.none();\n\n const recordValue = pipe(\n Option.some(\"rec-1\"),\n Option.flatMap((id) => getRecord(id)),\n Option.map((rec) => rec.nested),\n Option.map((nested) => nested.value),\n Option.map((value) => value * 2)\n );\n\n const finalValue = Option.getOrElse(recordValue, () => 0);\n\n yield* Effect.log(`[VALUE] ${finalValue}`);\n\n // Missing record\n const missingValue = pipe(\n Option.some(\"rec-999\"),\n Option.flatMap((id) => getRecord(id)),\n Option.map((rec) => rec.nested),\n Option.map((nested) => nested.value),\n Option.map((value) => value * 2)\n );\n\n const defaultValue = Option.getOrElse(missingValue, () => 0);\n\n yield* Effect.log(`[DEFAULT] ${defaultValue}\\n`);\n\n // Example 9: Conditional chaining\n console.log(`[9] Conditional paths:\\n`);\n\n const loadUserWithFallback = (id: string) =>\n pipe(\n findUser(id),\n Option.flatMap((user) =>\n // Only get premium features if user exists\n user.name.includes(\"Alice\")\n ? Option.some({ ...user, isPremium: true })\n : Option.none()\n ),\n Option.orElse(() =>\n // Fallback: return basic user\n findUser(id)\n )\n );\n\n const result1 = loadUserWithFallback(\"user-42\");\n const result2 = loadUserWithFallback(\"user-99\");\n\n yield* Effect.log(\n `[CONDITIONAL 1] ${Option.match(result1, { onSome: (u) => `${u.name}`, onNone: () => \"Not found\" })}`\n );\n\n yield* Effect.log(\n `[CONDITIONAL 2] ${Option.match(result2, { onSome: (u) => `${u.name}`, onNone: () => \"Not found\" })}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Monadic Comprehension\n\nUse do-notation for complex chains:\n\n```typescript\nconst complexChain = pipe(\n Option.Do,\n Option.bind(\"user\", () => findUser(\"user-42\")),\n Option.bind(\"profile\", ({ user }) => getProfile(user.id)),\n Option.bind(\"settings\", ({ user }) => getSettings(user.id)),\n Option.map(({ user, profile, settings }) => ({\n name: user.name,\n bio: profile.bio,\n theme: settings.theme,\n }))\n);\n\n// Or with effect.gen syntax\nconst withGen = Effect.gen(function* () {\n const user = yield* Option.fromNullable(findUser(\"user-42\"));\n const profile = yield* Option.fromNullable(getProfile(user.id));\n const settings = yield* Option.fromNullable(getSettings(user.id));\n\n return { user, profile, settings };\n});\n```\n\n---\n\n## Advanced: Collecting Multiple Options\n\nCombine multiple independent Option values:\n\n```typescript\nconst combineAll = (options: Option.Option[]): Option.Option =>\n options.reduce(\n (acc, opt) =>\n pipe(\n acc,\n Option.flatMap((values) =>\n Option.map(opt, (value) => [...values, value])\n )\n ),\n Option.some([])\n );\n\n// Usage: Load all or nothing\nconst results = combineAll([\n findUser(\"user-1\"),\n findUser(\"user-2\"),\n findUser(\"user-3\"),\n]);\n\n// Gets Some([user1, user2, user3]) or None if any missing\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use chaining when:**\n- Multiple dependent operations\n- Each returns Option\n- Clean data flow needed\n- Avoiding null checks\n- Readable pipelines\n\n✅ **Use composition when:**\n- Reusable transformations\n- Complex business logic\n- Multiple alternative paths\n- Fallback chains\n\n⚠️ **Trade-offs:**\n- Mental model shift required\n- More operators to learn\n- Potential performance overhead (negligible)\n- Debugging chains harder\n\n---\n\n## Chain Patterns\n\n| Pattern | Use Case | Example |\n| --- | --- | --- |\n| **map** | Transform value | `Option.map(opt, x => x * 2)` |\n| **flatMap** | Chain operations | `Option.flatMap(opt, x => getOther(x))` |\n| **ap** | Apply function | `Option.ap(func, arg)` |\n| **traverse** | Map over array | `Option.all([opt1, opt2])` |\n| **orElse** | Fallback | `Option.orElse(opt, alt)` |\n\n---\n\n## See Also\n\n- [Optional Pattern 1: Handling None/Some](./optional-pattern-handling-none-some.mdx) - Basics\n- [Stream Pattern 1: Map & Filter](./stream-pattern-map-filter-transformations.mdx) - Stream chaining\n- [Stream Pattern 2: Merge & Combine](./stream-pattern-merge-combine.mdx) - Stream composition\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error chains" - }, - { - "id": "organize-layers-into-composable-modules", - "title": "Organize Layers into Composable Modules", - "description": "Organize services into modular Layers that are composed hierarchically to manage complexity in large applications.", - "skillLevel": "advanced", - "useCases": [ - "testing" - ], - "example": "This example shows a `BaseLayer` with a `Logger`, a `UserModule` that uses the `Logger`, and a final `AppLayer` that wires them together.\n\n### 1. The Base Infrastructure Layer\n\n```typescript\n// src/core/Logger.ts\nimport { Effect } from \"effect\";\n\nexport class Logger extends Effect.Service()(\"App/Core/Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.log(`[LOG] ${msg}`),\n }),\n}) {}\n\n// src/features/User/UserRepository.ts\nexport class UserRepository extends Effect.Service()(\n \"App/User/UserRepository\",\n {\n // Define implementation that uses Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n findById: (id: number) =>\n Effect.gen(function* () {\n yield* logger.log(`Finding user ${id}`);\n return { id, name: `User ${id}` };\n }),\n };\n }),\n // Declare Logger dependency\n dependencies: [Logger.Default],\n }\n) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const repo = yield* UserRepository;\n const user = yield* repo.findById(1);\n return user;\n});\n\n// Run with default implementations\nEffect.runPromise(Effect.provide(program, UserRepository.Default));\n\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Program result: ${JSON.stringify(result)}`);\n return result;\n});\n\nEffect.runPromise(Effect.provide(programWithLogging, UserRepository.Default));\n```\n\n### 2. The Feature Module Layer\n\n```typescript\n// src/core/Logger.ts\nimport { Effect } from \"effect\";\n\nexport class Logger extends Effect.Service()(\"App/Core/Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)),\n }),\n}) {}\n\n// src/features/User/UserRepository.ts\nexport class UserRepository extends Effect.Service()(\n \"App/User/UserRepository\",\n {\n // Define implementation that uses Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n findById: (id: number) =>\n Effect.gen(function* () {\n yield* logger.log(`Finding user ${id}`);\n return { id, name: `User ${id}` };\n }),\n };\n }),\n // Declare Logger dependency\n dependencies: [Logger.Default],\n }\n) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const repo = yield* UserRepository;\n const user = yield* repo.findById(1);\n return user;\n});\n\n// Run with default implementations\nEffect.runPromise(Effect.provide(program, UserRepository.Default)).then(\n console.log\n);\n```\n\n### 3. The Final Application Composition\n\n```typescript\n// src/layers.ts\nimport { Layer } from \"effect\";\nimport { BaseLayer } from \"./core\";\nimport { UserModuleLive } from \"./features/User\";\n// import { ProductModuleLive } from \"./features/Product\";\n\nconst AllModules = Layer.mergeAll(UserModuleLive /*, ProductModuleLive */);\n\n// Provide the BaseLayer to all modules at once, creating a self-contained AppLayer.\nexport const AppLayer = Layer.provide(AllModules, BaseLayer);\n```\n\n---", - "antiPattern": "A flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage.\n\n```typescript\n// ❌ This file becomes huge and hard to navigate in a large project.\nconst AppLayer = Layer.mergeAll(\n LoggerLive,\n ConfigLive,\n DatabaseLive,\n TracerLive,\n UserServiceLive,\n UserRepositoryLive,\n ProductServiceLive,\n ProductRepositoryLive,\n BillingServiceLive\n // ...and 50 other services\n);\n```", - "explanation": "As an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure.\n\nThis approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked `BaseLayer`.\n\n---", - "content": "## Guideline\n\nFor large applications, avoid a single, flat list of services. Instead, structure your application by creating hierarchical layers:\n\n1. **`BaseLayer`**: Provides application-wide infrastructure (Logger, Config, Database).\n2. **`FeatureModule` Layers**: Provide the services for a specific business domain (e.g., `UserModule`, `ProductModule`). These depend on the `BaseLayer`.\n3. **`AppLayer`**: The top-level layer that composes the feature modules by providing them with the `BaseLayer`.\n\n---\n\n## Rationale\n\nAs an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure.\n\nThis approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked `BaseLayer`.\n\n---\n\n## Good Example\n\nThis example shows a `BaseLayer` with a `Logger`, a `UserModule` that uses the `Logger`, and a final `AppLayer` that wires them together.\n\n### 1. The Base Infrastructure Layer\n\n```typescript\n// src/core/Logger.ts\nimport { Effect } from \"effect\";\n\nexport class Logger extends Effect.Service()(\"App/Core/Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.log(`[LOG] ${msg}`),\n }),\n}) {}\n\n// src/features/User/UserRepository.ts\nexport class UserRepository extends Effect.Service()(\n \"App/User/UserRepository\",\n {\n // Define implementation that uses Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n findById: (id: number) =>\n Effect.gen(function* () {\n yield* logger.log(`Finding user ${id}`);\n return { id, name: `User ${id}` };\n }),\n };\n }),\n // Declare Logger dependency\n dependencies: [Logger.Default],\n }\n) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const repo = yield* UserRepository;\n const user = yield* repo.findById(1);\n return user;\n});\n\n// Run with default implementations\nEffect.runPromise(Effect.provide(program, UserRepository.Default));\n\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Program result: ${JSON.stringify(result)}`);\n return result;\n});\n\nEffect.runPromise(Effect.provide(programWithLogging, UserRepository.Default));\n```\n\n### 2. The Feature Module Layer\n\n```typescript\n// src/core/Logger.ts\nimport { Effect } from \"effect\";\n\nexport class Logger extends Effect.Service()(\"App/Core/Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)),\n }),\n}) {}\n\n// src/features/User/UserRepository.ts\nexport class UserRepository extends Effect.Service()(\n \"App/User/UserRepository\",\n {\n // Define implementation that uses Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n findById: (id: number) =>\n Effect.gen(function* () {\n yield* logger.log(`Finding user ${id}`);\n return { id, name: `User ${id}` };\n }),\n };\n }),\n // Declare Logger dependency\n dependencies: [Logger.Default],\n }\n) {}\n\n// Example usage\nconst program = Effect.gen(function* () {\n const repo = yield* UserRepository;\n const user = yield* repo.findById(1);\n return user;\n});\n\n// Run with default implementations\nEffect.runPromise(Effect.provide(program, UserRepository.Default)).then(\n console.log\n);\n```\n\n### 3. The Final Application Composition\n\n```typescript\n// src/layers.ts\nimport { Layer } from \"effect\";\nimport { BaseLayer } from \"./core\";\nimport { UserModuleLive } from \"./features/User\";\n// import { ProductModuleLive } from \"./features/Product\";\n\nconst AllModules = Layer.mergeAll(UserModuleLive /*, ProductModuleLive */);\n\n// Provide the BaseLayer to all modules at once, creating a self-contained AppLayer.\nexport const AppLayer = Layer.provide(AllModules, BaseLayer);\n```\n\n---\n\n## Anti-Pattern\n\nA flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage.\n\n```typescript\n// ❌ This file becomes huge and hard to navigate in a large project.\nconst AppLayer = Layer.mergeAll(\n LoggerLive,\n ConfigLive,\n DatabaseLive,\n TracerLive,\n UserServiceLive,\n UserRepositoryLive,\n ProductServiceLive,\n ProductRepositoryLive,\n BillingServiceLive\n // ...and 50 other services\n);\n```" - }, - { - "id": "parse-with-schema-decode", - "title": "Parse and Validate Data with Schema.decode", - "description": "Parse and validate data with Schema.decode.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Effect, Schema } from \"effect\";\n\ninterface User {\n name: string;\n}\n\nconst UserSchema = Schema.Struct({\n name: Schema.String,\n}) as Schema.Schema;\n\nconst processUserInput = (input: unknown) =>\n Effect.gen(function* () {\n const user = yield* Schema.decodeUnknown(UserSchema)(input);\n return `Welcome, ${user.name}!`;\n }).pipe(\n Effect.catchTag(\"ParseError\", () => Effect.succeed(\"Invalid user data.\"))\n );\n\n// Demonstrate the schema parsing\nconst program = Effect.gen(function* () {\n // Test with valid input\n const validInput = { name: \"Paul\" };\n const validResult = yield* processUserInput(validInput);\n yield* Effect.logInfo(`Valid input result: ${validResult}`);\n\n // Test with invalid input\n const invalidInput = { age: 25 }; // Missing 'name' field\n const invalidResult = yield* processUserInput(invalidInput);\n yield* Effect.logInfo(`Invalid input result: ${invalidResult}`);\n\n // Test with completely invalid input\n const badInput = \"not an object\";\n const badResult = yield* processUserInput(badInput);\n yield* Effect.logInfo(`Bad input result: ${badResult}`);\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Schema.decode` integrates parsing and validation into the Effect workflow,\nmaking error handling composable and type-safe.", - "antiPattern": "Using `Schema.parse(schema)(input)`, as it throws an exception. This forces\nyou to use `try/catch` blocks, which breaks the composability of Effect.", - "explanation": "Unlike the older `Schema.parse` which throws, `Schema.decode` is fully\nintegrated into the Effect ecosystem, allowing you to handle validation\nfailures gracefully with operators like `Effect.catchTag`.", - "content": "# Parse and Validate Data with Schema.decode\n\n## Guideline\n\nWhen you need to parse or validate data against a `Schema`, use the\n`Schema.decode(schema)` function. It takes an `unknown` input and returns an\n`Effect`.\n\n## Rationale\n\nUnlike the older `Schema.parse` which throws, `Schema.decode` is fully\nintegrated into the Effect ecosystem, allowing you to handle validation\nfailures gracefully with operators like `Effect.catchTag`.\n\n## Good Example\n\n```typescript\nimport { Effect, Schema } from \"effect\";\n\ninterface User {\n name: string;\n}\n\nconst UserSchema = Schema.Struct({\n name: Schema.String,\n}) as Schema.Schema;\n\nconst processUserInput = (input: unknown) =>\n Effect.gen(function* () {\n const user = yield* Schema.decodeUnknown(UserSchema)(input);\n return `Welcome, ${user.name}!`;\n }).pipe(\n Effect.catchTag(\"ParseError\", () => Effect.succeed(\"Invalid user data.\"))\n );\n\n// Demonstrate the schema parsing\nconst program = Effect.gen(function* () {\n // Test with valid input\n const validInput = { name: \"Paul\" };\n const validResult = yield* processUserInput(validInput);\n yield* Effect.logInfo(`Valid input result: ${validResult}`);\n\n // Test with invalid input\n const invalidInput = { age: 25 }; // Missing 'name' field\n const invalidResult = yield* processUserInput(invalidInput);\n yield* Effect.logInfo(`Invalid input result: ${invalidResult}`);\n\n // Test with completely invalid input\n const badInput = \"not an object\";\n const badResult = yield* processUserInput(badInput);\n yield* Effect.logInfo(`Bad input result: ${badResult}`);\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Schema.decode` integrates parsing and validation into the Effect workflow,\nmaking error handling composable and type-safe.\n\n## Anti-Pattern\n\nUsing `Schema.parse(schema)(input)`, as it throws an exception. This forces\nyou to use `try/catch` blocks, which breaks the composability of Effect." - }, - { - "id": "http-json-responses", - "title": "Parse JSON Responses Safely", - "description": "Always validate HTTP responses with Schema to catch API changes at runtime.", - "skillLevel": "beginner", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Console } from \"effect\"\nimport { Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\nimport { NodeHttpClient, NodeRuntime } from \"@effect/platform-node\"\n\n// ============================================\n// 1. Define response schemas\n// ============================================\n\nconst PostSchema = Schema.Struct({\n id: Schema.Number,\n title: Schema.String,\n body: Schema.String,\n userId: Schema.Number,\n})\n\ntype Post = Schema.Schema.Type\n\nconst PostArraySchema = Schema.Array(PostSchema)\n\n// ============================================\n// 2. Fetch and validate single item\n// ============================================\n\nconst getPost = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/posts/${id}`\n )\n const json = yield* HttpClientResponse.json(response)\n\n // Validate against schema - fails if data doesn't match\n const post = yield* Schema.decodeUnknown(PostSchema)(json)\n\n return post\n })\n\n// ============================================\n// 3. Fetch and validate array\n// ============================================\n\nconst getPosts = Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n \"https://jsonplaceholder.typicode.com/posts\"\n )\n const json = yield* HttpClientResponse.json(response)\n\n // Validate array of posts\n const posts = yield* Schema.decodeUnknown(PostArraySchema)(json)\n\n return posts\n})\n\n// ============================================\n// 4. Handle validation errors\n// ============================================\n\nconst safeGetPost = (id: number) =>\n getPost(id).pipe(\n Effect.catchTag(\"ParseError\", (error) =>\n Effect.gen(function* () {\n yield* Console.error(`Invalid response format: ${error.message}`)\n // Return a default or fail differently\n return yield* Effect.fail(new Error(`Post ${id} has invalid format`))\n })\n )\n )\n\n// ============================================\n// 5. Schema with optional fields\n// ============================================\n\nconst UserSchema = Schema.Struct({\n id: Schema.Number,\n name: Schema.String,\n email: Schema.String,\n phone: Schema.optional(Schema.String), // May not exist\n website: Schema.optional(Schema.String),\n company: Schema.optional(\n Schema.Struct({\n name: Schema.String,\n catchPhrase: Schema.optional(Schema.String),\n })\n ),\n})\n\nconst getUser = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/users/${id}`\n )\n const json = yield* HttpClientResponse.json(response)\n\n return yield* Schema.decodeUnknown(UserSchema)(json)\n })\n\n// ============================================\n// 6. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Console.log(\"=== Validated Single Post ===\")\n const post = yield* getPost(1)\n yield* Console.log(`Title: ${post.title}`)\n\n yield* Console.log(\"\\n=== Validated Posts Array ===\")\n const posts = yield* getPosts\n yield* Console.log(`Fetched ${posts.length} posts`)\n\n yield* Console.log(\"\\n=== User with Optional Fields ===\")\n const user = yield* getUser(1)\n yield* Console.log(`User: ${user.name}`)\n yield* Console.log(`Company: ${user.company?.name ?? \"N/A\"}`)\n})\n\nprogram.pipe(\n Effect.provide(NodeHttpClient.layer),\n NodeRuntime.runMain\n)\n```", - "antiPattern": "", - "explanation": "APIs can change without warning:\n\n1. **Fields disappear** - Backend removes a field\n2. **Types change** - String becomes number\n3. **Nulls appear** - Required field becomes optional\n4. **New fields** - Extra data you didn't expect\n\nSchema validation catches these issues immediately.\n\n---", - "content": "## Guideline\n\nUse Effect Schema to validate HTTP JSON responses, ensuring the data matches your expected types at runtime.\n\n---\n\n## Rationale\n\nAPIs can change without warning:\n\n1. **Fields disappear** - Backend removes a field\n2. **Types change** - String becomes number\n3. **Nulls appear** - Required field becomes optional\n4. **New fields** - Extra data you didn't expect\n\nSchema validation catches these issues immediately.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Console } from \"effect\"\nimport { Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\nimport { NodeHttpClient, NodeRuntime } from \"@effect/platform-node\"\n\n// ============================================\n// 1. Define response schemas\n// ============================================\n\nconst PostSchema = Schema.Struct({\n id: Schema.Number,\n title: Schema.String,\n body: Schema.String,\n userId: Schema.Number,\n})\n\ntype Post = Schema.Schema.Type\n\nconst PostArraySchema = Schema.Array(PostSchema)\n\n// ============================================\n// 2. Fetch and validate single item\n// ============================================\n\nconst getPost = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/posts/${id}`\n )\n const json = yield* HttpClientResponse.json(response)\n\n // Validate against schema - fails if data doesn't match\n const post = yield* Schema.decodeUnknown(PostSchema)(json)\n\n return post\n })\n\n// ============================================\n// 3. Fetch and validate array\n// ============================================\n\nconst getPosts = Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n \"https://jsonplaceholder.typicode.com/posts\"\n )\n const json = yield* HttpClientResponse.json(response)\n\n // Validate array of posts\n const posts = yield* Schema.decodeUnknown(PostArraySchema)(json)\n\n return posts\n})\n\n// ============================================\n// 4. Handle validation errors\n// ============================================\n\nconst safeGetPost = (id: number) =>\n getPost(id).pipe(\n Effect.catchTag(\"ParseError\", (error) =>\n Effect.gen(function* () {\n yield* Console.error(`Invalid response format: ${error.message}`)\n // Return a default or fail differently\n return yield* Effect.fail(new Error(`Post ${id} has invalid format`))\n })\n )\n )\n\n// ============================================\n// 5. Schema with optional fields\n// ============================================\n\nconst UserSchema = Schema.Struct({\n id: Schema.Number,\n name: Schema.String,\n email: Schema.String,\n phone: Schema.optional(Schema.String), // May not exist\n website: Schema.optional(Schema.String),\n company: Schema.optional(\n Schema.Struct({\n name: Schema.String,\n catchPhrase: Schema.optional(Schema.String),\n })\n ),\n})\n\nconst getUser = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/users/${id}`\n )\n const json = yield* HttpClientResponse.json(response)\n\n return yield* Schema.decodeUnknown(UserSchema)(json)\n })\n\n// ============================================\n// 6. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Console.log(\"=== Validated Single Post ===\")\n const post = yield* getPost(1)\n yield* Console.log(`Title: ${post.title}`)\n\n yield* Console.log(\"\\n=== Validated Posts Array ===\")\n const posts = yield* getPosts\n yield* Console.log(`Fetched ${posts.length} posts`)\n\n yield* Console.log(\"\\n=== User with Optional Fields ===\")\n const user = yield* getUser(1)\n yield* Console.log(`User: ${user.name}`)\n yield* Console.log(`Company: ${user.company?.name ?? \"N/A\"}`)\n})\n\nprogram.pipe(\n Effect.provide(NodeHttpClient.layer),\n NodeRuntime.runMain\n)\n```\n\n## Schema Helpers\n\n| Schema | Purpose |\n|--------|---------|\n| `Schema.String` | String value |\n| `Schema.Number` | Number value |\n| `Schema.Boolean` | Boolean value |\n| `Schema.Struct({...})` | Object with fields |\n| `Schema.Array(schema)` | Array of items |\n| `Schema.optional(schema)` | May be undefined |\n| `Schema.NullOr(schema)` | May be null |\n\n## Error Handling\n\n```typescript\nEffect.catchTag(\"ParseError\", (error) => {\n // error.message contains validation details\n // error.actual contains the invalid data\n})\n```\n\n## Best Practices\n\n1. **Define schemas upfront** - Document expected API format\n2. **Use optional for unreliable fields** - APIs change\n3. **Handle ParseError** - Graceful degradation\n4. **Log validation failures** - Debug API issues" - }, - { - "id": "pattern-option-either-match", - "title": "Pattern Match on Option and Either", - "description": "Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values", - "skillLevel": "beginner", - "useCases": [ - "error-management" - ], - "example": "### Basic Option Matching\n\n```typescript\nimport { Option } from \"effect\";\n\nconst getUserName = (id: number): Option.Option => {\n return id === 1 ? Option.some(\"Alice\") : Option.none();\n};\n\n// Using .match() for declarative pattern matching\nconst displayUser = (id: number): string =>\n getUserName(id).pipe(\n Option.match({\n onNone: () => \"Guest User\",\n onSome: (name) => `Hello, ${name}!`,\n })\n );\n\nconsole.log(displayUser(1)); // \"Hello, Alice!\"\nconsole.log(displayUser(999)); // \"Guest User\"\n```\n\n### Basic Either Matching\n\n```typescript\nimport { Either } from \"effect\";\n\nconst validateAge = (age: number): Either.Either => {\n return age >= 18\n ? Either.right(age)\n : Either.left(\"Must be 18 or older\");\n};\n\n// Using .match() for error handling\nconst processAge = (age: number): string =>\n validateAge(age).pipe(\n Either.match({\n onLeft: (error) => `Validation failed: ${error}`,\n onRight: (validAge) => `Age ${validAge} is valid`,\n })\n );\n\nconsole.log(processAge(25)); // \"Age 25 is valid\"\nconsole.log(processAge(15)); // \"Validation failed: Must be 18 or older\"\n```\n\n### Advanced: Nested Matching\n\nWhen dealing with nested Option and Either, use nested `.match()` calls:\n\n```typescript\nimport { Option, Either } from \"effect\";\n\ninterface UserProfile {\n name: string;\n age: number;\n}\n\nconst getUserProfile = (\n id: number\n): Option.Option> => {\n if (id === 0) return Option.none(); // User not found\n if (id === 1) return Option.some(Either.left(\"Profile incomplete\"));\n return Option.some(Either.right({ name: \"Bob\", age: 25 }));\n};\n\n// Nested matching - first on Option, then on Either\nconst displayProfile = (id: number): string =>\n getUserProfile(id).pipe(\n Option.match({\n onNone: () => \"User not found\",\n onSome: (result) =>\n result.pipe(\n Either.match({\n onLeft: (error) => `Error: ${error}`,\n onRight: (profile) => `${profile.name} (${profile.age})`,\n })\n ),\n })\n );\n\nconsole.log(displayProfile(0)); // \"User not found\"\nconsole.log(displayProfile(1)); // \"Error: Profile incomplete\"\nconsole.log(displayProfile(2)); // \"Bob (25)\"\n```", - "antiPattern": "Avoid manual conditional checks and nested ternaries:\n\n```typescript\n// ❌ ANTI-PATTERN: Imperative checks with isSome/isLeft\nconst name = getUserName(1);\nlet result: string;\nif (Option.isSome(name)) {\n result = `Hello, ${name.value}!`;\n} else {\n result = \"Guest User\";\n}\n\n// ❌ ANTI-PATTERN: Nested ternaries\nconst ageResult = validateAge(25);\nconst message = ageResult.pipe(\n Either.match({\n onLeft: () => \"Invalid\",\n onRight: (age) => age >= 21 ? \"Can drink\" : \"Cannot drink\",\n })\n);\n\n// ❌ ANTI-PATTERN: Chained if-else instead of match\nfunction processValue(value: Option.Option): string {\n if (Option.isSome(value)) {\n if (value.value > 0) {\n return \"Positive\";\n } else if (value.value < 0) {\n return \"Negative\";\n } else {\n return \"Zero\";\n }\n }\n return \"No value\";\n}\n```\n\nWhy these are worse:\n- **Less readable**: The intent is hidden in imperative logic\n- **Error-prone**: Easy to forget cases or introduce bugs\n- **Mutable state**: Often requires intermediate variables\n- **Less composable**: Harder to pipe and combine operations", - "explanation": "The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because:\n\n1. **Declarative**: Expresses intent clearly - \"match on these cases\"\n2. **Type-safe**: TypeScript ensures all cases are handled\n3. **Exhaustive**: You can't accidentally miss a case\n4. **Composable**: Works naturally with `.pipe()` for chaining operations\n5. **Readable**: The structure mirrors the data type itself\n\nWithout `.match()`, you'd need imperative conditionals, which are harder to read and easier to get wrong.", - "content": "## Guideline\n\nWhen you need to handle `Option` or `Either` values, use the `.match()` combinator instead of imperative checks. The `.match()` method provides a declarative, exhaustive way to handle all cases (Some/None for Option, Right/Left for Either) in a single expression.\n\nUse `.match()` when:\n- You need to handle both success and failure cases\n- You want type-safe pattern matching\n- You prefer declarative over imperative code\n- You need to transform values based on their case\n\n## Rationale\n\nThe `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because:\n\n1. **Declarative**: Expresses intent clearly - \"match on these cases\"\n2. **Type-safe**: TypeScript ensures all cases are handled\n3. **Exhaustive**: You can't accidentally miss a case\n4. **Composable**: Works naturally with `.pipe()` for chaining operations\n5. **Readable**: The structure mirrors the data type itself\n\nWithout `.match()`, you'd need imperative conditionals, which are harder to read and easier to get wrong.\n\n## Good Example\n\n### Basic Option Matching\n\n```typescript\nimport { Option } from \"effect\";\n\nconst getUserName = (id: number): Option.Option => {\n return id === 1 ? Option.some(\"Alice\") : Option.none();\n};\n\n// Using .match() for declarative pattern matching\nconst displayUser = (id: number): string =>\n getUserName(id).pipe(\n Option.match({\n onNone: () => \"Guest User\",\n onSome: (name) => `Hello, ${name}!`,\n })\n );\n\nconsole.log(displayUser(1)); // \"Hello, Alice!\"\nconsole.log(displayUser(999)); // \"Guest User\"\n```\n\n### Basic Either Matching\n\n```typescript\nimport { Either } from \"effect\";\n\nconst validateAge = (age: number): Either.Either => {\n return age >= 18\n ? Either.right(age)\n : Either.left(\"Must be 18 or older\");\n};\n\n// Using .match() for error handling\nconst processAge = (age: number): string =>\n validateAge(age).pipe(\n Either.match({\n onLeft: (error) => `Validation failed: ${error}`,\n onRight: (validAge) => `Age ${validAge} is valid`,\n })\n );\n\nconsole.log(processAge(25)); // \"Age 25 is valid\"\nconsole.log(processAge(15)); // \"Validation failed: Must be 18 or older\"\n```\n\n### Advanced: Nested Matching\n\nWhen dealing with nested Option and Either, use nested `.match()` calls:\n\n```typescript\nimport { Option, Either } from \"effect\";\n\ninterface UserProfile {\n name: string;\n age: number;\n}\n\nconst getUserProfile = (\n id: number\n): Option.Option> => {\n if (id === 0) return Option.none(); // User not found\n if (id === 1) return Option.some(Either.left(\"Profile incomplete\"));\n return Option.some(Either.right({ name: \"Bob\", age: 25 }));\n};\n\n// Nested matching - first on Option, then on Either\nconst displayProfile = (id: number): string =>\n getUserProfile(id).pipe(\n Option.match({\n onNone: () => \"User not found\",\n onSome: (result) =>\n result.pipe(\n Either.match({\n onLeft: (error) => `Error: ${error}`,\n onRight: (profile) => `${profile.name} (${profile.age})`,\n })\n ),\n })\n );\n\nconsole.log(displayProfile(0)); // \"User not found\"\nconsole.log(displayProfile(1)); // \"Error: Profile incomplete\"\nconsole.log(displayProfile(2)); // \"Bob (25)\"\n```\n\n## Anti-Pattern\n\nAvoid manual conditional checks and nested ternaries:\n\n```typescript\n// ❌ ANTI-PATTERN: Imperative checks with isSome/isLeft\nconst name = getUserName(1);\nlet result: string;\nif (Option.isSome(name)) {\n result = `Hello, ${name.value}!`;\n} else {\n result = \"Guest User\";\n}\n\n// ❌ ANTI-PATTERN: Nested ternaries\nconst ageResult = validateAge(25);\nconst message = ageResult.pipe(\n Either.match({\n onLeft: () => \"Invalid\",\n onRight: (age) => age >= 21 ? \"Can drink\" : \"Cannot drink\",\n })\n);\n\n// ❌ ANTI-PATTERN: Chained if-else instead of match\nfunction processValue(value: Option.Option): string {\n if (Option.isSome(value)) {\n if (value.value > 0) {\n return \"Positive\";\n } else if (value.value < 0) {\n return \"Negative\";\n } else {\n return \"Zero\";\n }\n }\n return \"No value\";\n}\n```\n\nWhy these are worse:\n- **Less readable**: The intent is hidden in imperative logic\n- **Error-prone**: Easy to forget cases or introduce bugs\n- **Mutable state**: Often requires intermediate variables\n- **Less composable**: Harder to pipe and combine operations\n\n## Trade-offs\n\n### Pros\n- **Type-safe**: Exhaustiveness checking by TypeScript\n- **Declarative**: Clear intent, easy to understand\n- **Composable**: Works naturally with `.pipe()` chains\n- **Refactor-friendly**: Changes to the data type are caught at compile time\n- **Predictable**: Behavior is explicit and visible\n\n### Cons\n- **Slightly more verbose**: Compared to a simple `if` statement for trivial cases\n- **Learning curve**: Developers unfamiliar with FP may need adjustment\n- **Boilerplate**: For simple cases, `.match()` might feel like overkill\n\nFor most use cases, the benefits far outweigh the minor verbosity. The type safety and clarity make `.match()` the right choice." - }, - { - "id": "platform-pattern-command-execution", - "title": "Platform Pattern 1: Execute Shell Commands", - "description": "Use Command to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling.", - "skillLevel": "intermediate", - "useCases": [ - "platform" - ], - "example": "This example demonstrates executing commands and handling their output.\n\n```typescript\nimport { Command, Effect, Chunk } from \"@effect/platform\";\n\n// Simple command execution\nconst program = Effect.gen(function* () {\n console.log(`\\n[COMMAND] Executing shell commands\\n`);\n\n // Example 1: List files\n console.log(`[1] List files in current directory:\\n`);\n\n const lsResult = yield* Command.make(\"ls\", [\"-la\"]).pipe(\n Command.string\n );\n\n console.log(lsResult);\n\n // Example 2: Get current date\n console.log(`\\n[2] Get current date:\\n`);\n\n const dateResult = yield* Command.make(\"date\", [\"+%Y-%m-%d %H:%M:%S\"]).pipe(\n Command.string\n );\n\n console.log(`Current date: ${dateResult.trim()}`);\n\n // Example 3: Capture exit code\n console.log(`\\n[3] Check if file exists:\\n`);\n\n const fileCheckCmd = yield* Command.make(\"test\", [\n \"-f\",\n \"/etc/passwd\",\n ]).pipe(\n Command.exitCode,\n Effect.either\n );\n\n if (fileCheckCmd._tag === \"Right\") {\n console.log(`✓ File exists (exit code: 0)`);\n } else {\n console.log(`✗ File not found (exit code: ${fileCheckCmd.left})`);\n }\n\n // Example 4: Execute with custom working directory\n console.log(`\\n[4] List TypeScript files:\\n`);\n\n const findResult = yield* Command.make(\"find\", [\n \".\",\n \"-name\",\n \"*.ts\",\n \"-type\",\n \"f\",\n ]).pipe(\n Command.lines\n );\n\n const tsFiles = Chunk.take(findResult, 5); // First 5\n\n Chunk.forEach(tsFiles, (file) => {\n console.log(` - ${file}`);\n });\n\n if (Chunk.size(findResult) > 5) {\n console.log(` ... and ${Chunk.size(findResult) - 5} more`);\n }\n\n // Example 5: Handle command failure\n console.log(`\\n[5] Handle command failure gracefully:\\n`);\n\n const failResult = yield* Command.make(\"false\").pipe(\n Command.exitCode,\n Effect.catchAll((error) =>\n Effect.succeed(-1) // Return -1 for any error\n )\n );\n\n console.log(`Exit code: ${failResult}`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Shell integration without proper handling causes issues:\n\n- **Unhandled errors**: Non-zero exit codes lost\n- **Deadlocks**: Stdout buffer fills if not drained\n- **Resource leaks**: Processes left running\n- **Output loss**: stderr ignored\n- **Race conditions**: Unsafe concurrent execution\n\nCommand enables:\n\n- **Type-safe execution**: Success/failure handled in Effect\n- **Output capture**: Both stdout and stderr available\n- **Resource cleanup**: Automatic process termination\n- **Exit code handling**: Explicit error mapping\n\nReal-world example: Build pipeline\n- **Direct**: Process spawned, output mixed with app logs, exit code ignored\n- **With Command**: Output captured, exit code checked, errors propagated\n\n---", - "content": "## Guideline\n\nExecute shell commands with Command:\n\n- **Spawn**: Start external process\n- **Capture**: Get stdout/stderr/exit code\n- **Wait**: Block until completion\n- **Handle errors**: Exit codes indicate failure\n\nPattern: `Command.exec(\"command args\").pipe(...)`\n\n---\n\n## Rationale\n\nShell integration without proper handling causes issues:\n\n- **Unhandled errors**: Non-zero exit codes lost\n- **Deadlocks**: Stdout buffer fills if not drained\n- **Resource leaks**: Processes left running\n- **Output loss**: stderr ignored\n- **Race conditions**: Unsafe concurrent execution\n\nCommand enables:\n\n- **Type-safe execution**: Success/failure handled in Effect\n- **Output capture**: Both stdout and stderr available\n- **Resource cleanup**: Automatic process termination\n- **Exit code handling**: Explicit error mapping\n\nReal-world example: Build pipeline\n- **Direct**: Process spawned, output mixed with app logs, exit code ignored\n- **With Command**: Output captured, exit code checked, errors propagated\n\n---\n\n## Good Example\n\nThis example demonstrates executing commands and handling their output.\n\n```typescript\nimport { Command, Effect, Chunk } from \"@effect/platform\";\n\n// Simple command execution\nconst program = Effect.gen(function* () {\n console.log(`\\n[COMMAND] Executing shell commands\\n`);\n\n // Example 1: List files\n console.log(`[1] List files in current directory:\\n`);\n\n const lsResult = yield* Command.make(\"ls\", [\"-la\"]).pipe(\n Command.string\n );\n\n console.log(lsResult);\n\n // Example 2: Get current date\n console.log(`\\n[2] Get current date:\\n`);\n\n const dateResult = yield* Command.make(\"date\", [\"+%Y-%m-%d %H:%M:%S\"]).pipe(\n Command.string\n );\n\n console.log(`Current date: ${dateResult.trim()}`);\n\n // Example 3: Capture exit code\n console.log(`\\n[3] Check if file exists:\\n`);\n\n const fileCheckCmd = yield* Command.make(\"test\", [\n \"-f\",\n \"/etc/passwd\",\n ]).pipe(\n Command.exitCode,\n Effect.either\n );\n\n if (fileCheckCmd._tag === \"Right\") {\n console.log(`✓ File exists (exit code: 0)`);\n } else {\n console.log(`✗ File not found (exit code: ${fileCheckCmd.left})`);\n }\n\n // Example 4: Execute with custom working directory\n console.log(`\\n[4] List TypeScript files:\\n`);\n\n const findResult = yield* Command.make(\"find\", [\n \".\",\n \"-name\",\n \"*.ts\",\n \"-type\",\n \"f\",\n ]).pipe(\n Command.lines\n );\n\n const tsFiles = Chunk.take(findResult, 5); // First 5\n\n Chunk.forEach(tsFiles, (file) => {\n console.log(` - ${file}`);\n });\n\n if (Chunk.size(findResult) > 5) {\n console.log(` ... and ${Chunk.size(findResult) - 5} more`);\n }\n\n // Example 5: Handle command failure\n console.log(`\\n[5] Handle command failure gracefully:\\n`);\n\n const failResult = yield* Command.make(\"false\").pipe(\n Command.exitCode,\n Effect.catchAll((error) =>\n Effect.succeed(-1) // Return -1 for any error\n )\n );\n\n console.log(`Exit code: ${failResult}`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Run Multiple Commands in Sequence\n\nChain command executions:\n\n```typescript\nconst buildPipeline = Effect.gen(function* () {\n console.log(`[BUILD] Starting build pipeline\\n`);\n\n // Step 1: Clean\n console.log(`[STEP 1] Cleaning...\\n`);\n\n yield* Command.make(\"rm\", [\"-rf\", \"dist\"]).pipe(\n Command.exitCode,\n Effect.tap((code) =>\n Effect.log(`Clean: exit code ${code}`)\n )\n );\n\n // Step 2: Compile\n console.log(`[STEP 2] Compiling...\\n`);\n\n const compileOutput = yield* Command.make(\"tsc\", []).pipe(\n Command.string,\n Effect.catchAll((error) => {\n console.error(`Compile failed: ${error}`);\n return Effect.fail(error);\n })\n );\n\n yield* Effect.log(`Compile output:\\n${compileOutput}`);\n\n // Step 3: Test\n console.log(`[STEP 3] Running tests...\\n`);\n\n const testOutput = yield* Command.make(\"npm\", [\"test\"]).pipe(\n Command.string\n );\n\n yield* Effect.log(`Tests: ${testOutput.includes(\"pass\") ? \"✓\" : \"✗\"}`);\n\n // Step 4: Report\n console.log(`[STEP 4] Build complete\\n`);\n\n return { status: \"success\" };\n});\n```\n\n---\n\n## Advanced: Streaming Command Output\n\nProcess output line-by-line:\n\n```typescript\nconst streamCommandOutput = (\n command: string,\n args: string[]\n): Stream.Stream =>\n Command.make(command, args).pipe(\n Command.lines,\n Stream.fromChunk\n );\n\n// Usage: Process log file line-by-line\nconst logProcessing = streamCommandOutput(\"tail\", [\"-f\", \"/var/log/system.log\"]).pipe(\n Stream.filter((line) => line.includes(\"ERROR\")),\n Stream.tap((line) =>\n Effect.log(`[ERROR LOG] ${line}`)\n ),\n Stream.take(10),\n Stream.runDrain\n);\n\n// Usage: Process command output with transformation\nconst fileStats = streamCommandOutput(\"ls\", [\"-lh\"]).pipe(\n Stream.drop(1), // Skip header\n Stream.map((line) => {\n const parts = line.split(/\\s+/);\n return { size: parts[4], name: parts[8] };\n }),\n Stream.tap((stat) =>\n Effect.log(`File: ${stat.name} (${stat.size})`)\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## Advanced: Command with Environment Variables\n\nSet environment for command execution:\n\n```typescript\nconst commandWithEnv = Effect.gen(function* () {\n // Execute command with custom environment\n const result = yield* Command.make(\"printenv\", [\"MY_VAR\"]).pipe(\n Command.env((env) => ({\n ...env,\n MY_VAR: \"custom-value\",\n NODE_ENV: \"production\",\n })),\n Command.string,\n Effect.map((output) => output.trim())\n );\n\n yield* Effect.log(`MY_VAR = ${result}`);\n});\n\n// Usage: Build with environment variables\nconst buildWithEnv = Effect.gen(function* () {\n const result = yield* Command.make(\"npm\", [\"run\", \"build\"]).pipe(\n Command.env((env) => ({\n ...env,\n NODE_ENV: \"production\",\n SKIP_TESTS: \"true\",\n })),\n Command.exitCode\n );\n\n yield* Effect.log(`Build exit code: ${result}`);\n});\n```\n\n---\n\n## Advanced: Parallel Command Execution\n\nRun multiple commands concurrently:\n\n```typescript\nconst parallelCommands = Effect.gen(function* () {\n console.log(`[PARALLEL] Running 3 commands concurrently\\n`);\n\n const cmd1 = Command.make(\"npm\", [\"list\"]).pipe(Command.string);\n const cmd2 = Command.make(\"git\", [\"status\"]).pipe(Command.string);\n const cmd3 = Command.make(\"node\", [\"-v\"]).pipe(Command.string);\n\n // Execute in parallel\n const [result1, result2, result3] = yield* Effect.all(\n [cmd1, cmd2, cmd3],\n { concurrency: 3 } // Run 3 at once\n );\n\n console.log(`\\n[NPM]\\n${result1}`);\n console.log(`\\n[GIT]\\n${result2}`);\n console.log(`\\n[NODE]\\n${result3}`);\n});\n```\n\n---\n\n## Advanced: Timeout and Cancellation\n\nSet execution timeouts:\n\n```typescript\nconst commandWithTimeout = (\n command: string,\n args: string[],\n timeoutMs: number\n) =>\n Command.make(command, args).pipe(\n Command.string,\n Effect.timeout(`${timeoutMs} millis`),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `Command timed out after ${timeoutMs}ms`\n );\n return Effect.fail(error);\n })\n )\n );\n\n// Usage: Long-running build with 30 second timeout\nconst buildWithTimeout = commandWithTimeout(\"npm\", [\"run\", \"build\"], 30000).pipe(\n Effect.tap((output) =>\n Effect.log(`Build completed:\\n${output}`)\n ),\n Effect.catchAll((error) =>\n Effect.log(`Build failed: ${error.message}`)\n )\n);\n```\n\n---\n\n## Advanced: Command Retry with Backoff\n\nRetry failed commands:\n\n```typescript\nconst commandWithRetry = (\n command: string,\n args: string[],\n maxRetries: number\n) =>\n Command.make(command, args).pipe(\n Command.exitCode,\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.upTo(`5 seconds`),\n Schedule.compose(Schedule.recurs(maxRetries))\n )\n ),\n Effect.tap((code) =>\n Effect.log(`Command succeeded with exit code ${code}`)\n ),\n Effect.catchAll((error) =>\n Effect.log(`Command failed after ${maxRetries} retries`)\n )\n );\n\n// Usage: Retry flaky network command\nconst flakyCurl = commandWithRetry(\n \"curl\",\n [\"https://api.example.com/health\"],\n 3\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Command when:**\n\n- Executing external programs\n- Running shell scripts\n- System integration tasks\n- Build pipeline steps\n- Running CLI tools\n- Background process execution\n\n⚠️ **Trade-offs:**\n\n- External process overhead\n- Output parsing required (no schema)\n- Cross-platform command differences\n- Shell injection risks (sanitize inputs)\n- Resource consumption\n\n---\n\n## Security Considerations\n\n**Avoid shell injection:**\n```typescript\n// ❌ UNSAFE - Input not escaped\nCommand.make(\"echo\", [`Hello ${userInput}`])\n\n// ✅ SAFE - Input as separate argument\nCommand.make(\"echo\", [userInput])\n```\n\n**Validate/sanitize inputs:**\n```typescript\nconst safePath = path.replace(/[^\\w.-]/g, ''); // Remove special chars\nCommand.make(\"ls\", [safePath])\n```\n\n---\n\n## See Also\n\n- [Platform Pattern 2: FileSystem Operations](./platform-filesystem-operations.mdx) - File I/O\n- [Handle Side Effects with Effect.sync](./handle-side-effects-with-effect-sync.mdx) - Side effects\n- [Handle Errors with Catch](./handle-errors-with-catch.mdx) - Error handling\n- [Scheduling Pattern 2: Exponential Backoff](./scheduling-pattern-exponential-backoff.mdx) - Retry strategy" - }, - { - "id": "platform-filesystem-operations", - "title": "Platform Pattern 2: Filesystem Operations", - "description": "Use FileSystem module for safe, resource-managed file operations with proper error handling and cleanup.", - "skillLevel": "beginner", - "useCases": [ - "platform" - ], - "example": "This example demonstrates reading, writing, and manipulating files.\n\n```typescript\nimport { FileSystem, Effect, Stream } from \"@effect/platform\";\nimport * as fs from \"fs/promises\";\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[FILESYSTEM] Demonstrating file operations\\n`);\n\n // Example 1: Write a file\n console.log(`[1] Writing file:\\n`);\n\n const content = `Hello, Effect-TS!\\nThis is a test file.\\nCreated at ${new Date().toISOString()}`;\n\n yield* FileSystem.writeFileUtf8(\"test.txt\", content);\n\n yield* Effect.log(`✓ File written: test.txt`);\n\n // Example 2: Read the file\n console.log(`\\n[2] Reading file:\\n`);\n\n const readContent = yield* FileSystem.readFileUtf8(\"test.txt\");\n\n console.log(readContent);\n\n // Example 3: Get file stats\n console.log(`\\n[3] File stats:\\n`);\n\n const stats = yield* FileSystem.stat(\"test.txt\").pipe(\n Effect.flatMap((stat) =>\n Effect.succeed({\n size: stat.size,\n isFile: stat.isFile(),\n modified: stat.mtimeMs,\n })\n )\n );\n\n console.log(` Size: ${stats.size} bytes`);\n console.log(` Is file: ${stats.isFile}`);\n console.log(` Modified: ${new Date(stats.modified).toISOString()}`);\n\n // Example 4: Create directory and write multiple files\n console.log(`\\n[4] Creating directory and files:\\n`);\n\n yield* FileSystem.mkdir(\"test-dir\");\n\n yield* Effect.all(\n Array.from({ length: 3 }, (_, i) =>\n FileSystem.writeFileUtf8(\n `test-dir/file-${i + 1}.txt`,\n `Content of file ${i + 1}`\n )\n )\n );\n\n yield* Effect.log(`✓ Created directory with 3 files`);\n\n // Example 5: List directory contents\n console.log(`\\n[5] Listing directory:\\n`);\n\n const entries = yield* FileSystem.readDirectory(\"test-dir\");\n\n entries.forEach((entry) => {\n console.log(` - ${entry}`);\n });\n\n // Example 6: Append to file\n console.log(`\\n[6] Appending to file:\\n`);\n\n const appendContent = `\\nAppended line at ${new Date().toISOString()}`;\n\n yield* FileSystem.appendFileUtf8(\"test.txt\", appendContent);\n\n const finalContent = yield* FileSystem.readFileUtf8(\"test.txt\");\n\n console.log(`File now has ${finalContent.split(\"\\n\").length} lines`);\n\n // Example 7: Clean up\n console.log(`\\n[7] Cleaning up:\\n`);\n\n yield* Effect.all(\n Array.from({ length: 3 }, (_, i) =>\n FileSystem.remove(`test-dir/file-${i + 1}.txt`)\n )\n );\n\n yield* FileSystem.remove(\"test-dir\");\n yield* FileSystem.remove(\"test.txt\");\n\n yield* Effect.log(`✓ Cleanup complete`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Direct file operations without FileSystem create issues:\n\n- **Resource leaks**: Files not closed on errors\n- **No error context**: Missing file names in errors\n- **Blocking**: No async/await integration\n- **Cross-platform**: Path handling differences\n\nFileSystem enables:\n\n- **Resource safety**: Automatic cleanup\n- **Error context**: Full error messages\n- **Async integration**: Effect-native\n- **Cross-platform**: Handles path separators\n\nReal-world example: Process log files\n- **Direct**: Open file, read, close, handle exceptions manually\n- **With FileSystem**: `FileSystem.read(path).pipe(...)`\n\n---", - "content": "## Guideline\n\nFileSystem operations:\n\n- **read**: Read file as string\n- **readDirectory**: List files in directory\n- **write**: Write string to file\n- **remove**: Delete file or directory\n- **stat**: Get file metadata\n\nPattern: `FileSystem.read(path).pipe(...)`\n\n---\n\n## Rationale\n\nDirect file operations without FileSystem create issues:\n\n- **Resource leaks**: Files not closed on errors\n- **No error context**: Missing file names in errors\n- **Blocking**: No async/await integration\n- **Cross-platform**: Path handling differences\n\nFileSystem enables:\n\n- **Resource safety**: Automatic cleanup\n- **Error context**: Full error messages\n- **Async integration**: Effect-native\n- **Cross-platform**: Handles path separators\n\nReal-world example: Process log files\n- **Direct**: Open file, read, close, handle exceptions manually\n- **With FileSystem**: `FileSystem.read(path).pipe(...)`\n\n---\n\n## Good Example\n\nThis example demonstrates reading, writing, and manipulating files.\n\n```typescript\nimport { FileSystem, Effect, Stream } from \"@effect/platform\";\nimport * as fs from \"fs/promises\";\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[FILESYSTEM] Demonstrating file operations\\n`);\n\n // Example 1: Write a file\n console.log(`[1] Writing file:\\n`);\n\n const content = `Hello, Effect-TS!\\nThis is a test file.\\nCreated at ${new Date().toISOString()}`;\n\n yield* FileSystem.writeFileUtf8(\"test.txt\", content);\n\n yield* Effect.log(`✓ File written: test.txt`);\n\n // Example 2: Read the file\n console.log(`\\n[2] Reading file:\\n`);\n\n const readContent = yield* FileSystem.readFileUtf8(\"test.txt\");\n\n console.log(readContent);\n\n // Example 3: Get file stats\n console.log(`\\n[3] File stats:\\n`);\n\n const stats = yield* FileSystem.stat(\"test.txt\").pipe(\n Effect.flatMap((stat) =>\n Effect.succeed({\n size: stat.size,\n isFile: stat.isFile(),\n modified: stat.mtimeMs,\n })\n )\n );\n\n console.log(` Size: ${stats.size} bytes`);\n console.log(` Is file: ${stats.isFile}`);\n console.log(` Modified: ${new Date(stats.modified).toISOString()}`);\n\n // Example 4: Create directory and write multiple files\n console.log(`\\n[4] Creating directory and files:\\n`);\n\n yield* FileSystem.mkdir(\"test-dir\");\n\n yield* Effect.all(\n Array.from({ length: 3 }, (_, i) =>\n FileSystem.writeFileUtf8(\n `test-dir/file-${i + 1}.txt`,\n `Content of file ${i + 1}`\n )\n )\n );\n\n yield* Effect.log(`✓ Created directory with 3 files`);\n\n // Example 5: List directory contents\n console.log(`\\n[5] Listing directory:\\n`);\n\n const entries = yield* FileSystem.readDirectory(\"test-dir\");\n\n entries.forEach((entry) => {\n console.log(` - ${entry}`);\n });\n\n // Example 6: Append to file\n console.log(`\\n[6] Appending to file:\\n`);\n\n const appendContent = `\\nAppended line at ${new Date().toISOString()}`;\n\n yield* FileSystem.appendFileUtf8(\"test.txt\", appendContent);\n\n const finalContent = yield* FileSystem.readFileUtf8(\"test.txt\");\n\n console.log(`File now has ${finalContent.split(\"\\n\").length} lines`);\n\n // Example 7: Clean up\n console.log(`\\n[7] Cleaning up:\\n`);\n\n yield* Effect.all(\n Array.from({ length: 3 }, (_, i) =>\n FileSystem.remove(`test-dir/file-${i + 1}.txt`)\n )\n );\n\n yield* FileSystem.remove(\"test-dir\");\n yield* FileSystem.remove(\"test.txt\");\n\n yield* Effect.log(`✓ Cleanup complete`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Stream Large Files\n\nProcess files line-by-line without loading into memory:\n\n```typescript\nconst processLargeFile = (filePath: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STREAMING] Processing large file: ${filePath}`);\n\n // Read as stream\n const fileStream = yield* FileSystem.readFileStream(filePath);\n\n const lineStream = fileStream.pipe(\n Stream.decodeText(\"utf8\"),\n Stream.splitLines,\n Stream.filter((line) => line.trim().length > 0)\n );\n\n const lineCount = yield* lineStream.pipe(\n Stream.runFold(0, (count) => count + 1)\n );\n\n yield* Effect.log(`File has ${lineCount} non-empty lines`);\n });\n```\n\n---\n\n## Advanced: Process Multiple Files\n\nRead and process directory of files:\n\n```typescript\nconst processDirFiles = (dirPath: string, extension: string) =>\n Effect.gen(function* () {\n const entries = yield* FileSystem.readDirectory(dirPath);\n\n const files = entries.filter((f) => f.endsWith(extension));\n\n const results = yield* Effect.all(\n files.map((file) =>\n FileSystem.readFileUtf8(`${dirPath}/${file}`).pipe(\n Effect.map((content) => ({\n file,\n lines: content.split(\"\\n\").length,\n size: content.length,\n }))\n )\n )\n );\n\n results.forEach((result) => {\n console.log(\n `${result.file}: ${result.lines} lines, ${result.size} bytes`\n );\n });\n\n return results;\n });\n```\n\n---\n\n## Advanced: Transactional File Operations\n\nAtomic file writes with backup:\n\n```typescript\nconst atomicWrite = (filePath: string, content: string) =>\n Effect.gen(function* () {\n const tempPath = `${filePath}.tmp`;\n const backupPath = `${filePath}.bak`;\n\n // Write to temp file\n yield* FileSystem.writeFileUtf8(tempPath, content);\n\n // Backup original if exists\n const exists = yield* FileSystem.exists(filePath).pipe(\n Effect.either\n );\n\n if (exists._tag === \"Right\" && exists.right) {\n yield* FileSystem.copy(filePath, backupPath);\n }\n\n // Move temp to target (atomic on most systems)\n yield* FileSystem.rename(tempPath, filePath);\n\n yield* Effect.log(`✓ Atomically wrote: ${filePath}`);\n });\n```\n\n---\n\n## Advanced: Watch File Changes\n\nMonitor file modifications:\n\n```typescript\nconst watchFile = (filePath: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[WATCH] Monitoring: ${filePath}`);\n\n // Simple polling implementation (production would use fs.watch)\n let lastModified = 0;\n\n while (true) {\n yield* Effect.sleep(\"1 second\");\n\n const stats = yield* FileSystem.stat(filePath).pipe(\n Effect.either\n );\n\n if (stats._tag === \"Right\") {\n const currentModified = stats.right.mtimeMs;\n\n if (currentModified > lastModified) {\n lastModified = currentModified;\n\n const content = yield* FileSystem.readFileUtf8(filePath);\n\n yield* Effect.log(\n `[CHANGED] ${new Date(currentModified).toISOString()}`\n );\n yield* Effect.log(`Content: ${content.substring(0, 100)}...`);\n }\n }\n }\n });\n```\n\n---\n\n## Advanced: Recursive Directory Operations\n\nTraverse directory tree:\n\n```typescript\nconst walkDirectory = (\n dirPath: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const entries = yield* FileSystem.readDirectory(dirPath);\n const allFiles: string[] = [];\n\n for (const entry of entries) {\n const fullPath = `${dirPath}/${entry}`;\n const stat = yield* FileSystem.stat(fullPath);\n\n if (stat.isFile()) {\n allFiles.push(fullPath);\n } else if (stat.isDirectory()) {\n const subFiles = yield* walkDirectory(fullPath);\n allFiles.push(...subFiles);\n }\n }\n\n return allFiles;\n });\n\n// Usage: Find all TypeScript files\nconst findTypeScriptFiles = walkDirectory(\".\").pipe(\n Effect.map((files) =>\n files.filter((f) => f.endsWith(\".ts\") || f.endsWith(\".tsx\"))\n ),\n Effect.tap((files) =>\n Effect.log(`Found ${files.length} TypeScript files`)\n )\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use FileSystem when:**\n\n- Reading/writing files\n- Configuration file operations\n- Log file processing\n- Batch file operations\n- Cross-platform file handling\n- Managing file metadata\n\n⚠️ **Trade-offs:**\n\n- Disk I/O slower than memory\n- Path handling varies per OS\n- Large file operations can block\n- Permissions issues on restricted systems\n\n---\n\n## Security Considerations\n\n**Path traversal prevention:**\n```typescript\n// ❌ UNSAFE - User input can traverse directories\nFileSystem.read(`/data/${userInput}`)\n\n// ✅ SAFE - Validate and normalize paths\nconst safePath = path.normalize(`/data/${userInput}`);\nif (!safePath.startsWith(\"/data/\")) {\n throw new Error(\"Path traversal attempt\");\n}\nFileSystem.read(safePath)\n```\n\n---\n\n## See Also\n\n- [Platform Pattern 1: Command Execution](./platform-pattern-command-execution.mdx) - External commands\n- [Manage Resource Lifecycles with Scope](./manage-resource-lifecycles-with-scope.mdx) - Resource cleanup\n- [Stream Pattern 1: Map & Filter](./stream-pattern-map-filter-transformations.mdx) - Stream processing\n- [Handle Errors with Catch](./handle-errors-with-catch.mdx) - Error handling" - }, - { - "id": "platform-keyvaluestore-persistence", - "title": "Platform Pattern 3: Persistent Key-Value Storage", - "description": "Use KeyValueStore for simple persistent storage of key-value pairs, enabling lightweight caching and session management.", - "skillLevel": "intermediate", - "useCases": [ - "platform" - ], - "example": "This example demonstrates storing and retrieving persistent data.\n\n```typescript\nimport { KeyValueStore, Effect } from \"@effect/platform\";\n\ninterface UserSession {\n readonly userId: string;\n readonly token: string;\n readonly expiresAt: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[KEYVALUESTORE] Persistent storage example\\n`);\n\n const store = yield* KeyValueStore.KeyValueStore;\n\n // Example 1: Store session data\n console.log(`[1] Storing session:\\n`);\n\n const session: UserSession = {\n userId: \"user-123\",\n token: \"token-abc-def\",\n expiresAt: Date.now() + 3600000, // 1 hour\n };\n\n yield* store.set(\"session:user-123\", JSON.stringify(session));\n\n yield* Effect.log(`✓ Session stored`);\n\n // Example 2: Retrieve stored data\n console.log(`\\n[2] Retrieving session:\\n`);\n\n const stored = yield* store.get(\"session:user-123\");\n\n if (stored._tag === \"Some\") {\n const retrievedSession = JSON.parse(stored.value) as UserSession;\n\n console.log(` User ID: ${retrievedSession.userId}`);\n console.log(` Token: ${retrievedSession.token}`);\n console.log(\n ` Expires: ${new Date(retrievedSession.expiresAt).toISOString()}`\n );\n }\n\n // Example 3: Check if key exists\n console.log(`\\n[3] Checking keys:\\n`);\n\n const hasSession = yield* store.has(\"session:user-123\");\n const hasOther = yield* store.has(\"session:user-999\");\n\n console.log(` Has session:user-123: ${hasSession}`);\n console.log(` Has session:user-999: ${hasOther}`);\n\n // Example 4: Store multiple cache entries\n console.log(`\\n[4] Caching API responses:\\n`);\n\n const apiResponses = [\n { endpoint: \"/api/users\", data: [{ id: 1, name: \"Alice\" }] },\n { endpoint: \"/api/posts\", data: [{ id: 1, title: \"First Post\" }] },\n { endpoint: \"/api/comments\", data: [] },\n ];\n\n yield* Effect.all(\n apiResponses.map((item) =>\n store.set(\n `cache:${item.endpoint}`,\n JSON.stringify(item.data)\n )\n )\n );\n\n yield* Effect.log(`✓ Cached ${apiResponses.length} endpoints`);\n\n // Example 5: Retrieve cache with expiration\n console.log(`\\n[5] Checking cached data:\\n`);\n\n for (const item of apiResponses) {\n const cached = yield* store.get(`cache:${item.endpoint}`);\n\n if (cached._tag === \"Some\") {\n const data = JSON.parse(cached.value);\n\n console.log(\n ` ${item.endpoint}: ${Array.isArray(data) ? data.length : 1} items`\n );\n }\n }\n\n // Example 6: Remove specific entry\n console.log(`\\n[6] Removing entry:\\n`);\n\n yield* store.remove(\"cache:/api/comments\");\n\n const removed = yield* store.has(\"cache:/api/comments\");\n\n console.log(` Exists after removal: ${removed}`);\n\n // Example 7: Iterate and count entries\n console.log(`\\n[7] Counting entries:\\n`);\n\n const allKeys = yield* store.entries.pipe(\n Effect.map((entries) => entries.length)\n );\n\n console.log(` Total entries: ${allKeys}`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Without persistent storage, transient data is lost:\n\n- **Session data**: Lost on restart\n- **Caches**: Rebuilt from scratch\n- **Configuration**: Hardcoded or file-based\n- **State**: Scattered across code\n\nKeyValueStore enables:\n\n- **Transparent persistence**: Automatic backend handling\n- **Simple API**: Key-value abstraction\n- **Pluggable backends**: Memory, filesystem, database\n- **Effect integration**: Type-safe, composable\n\nReal-world example: Caching API responses\n- **Direct**: Cache in memory Map (lost on restart)\n- **With KeyValueStore**: Persistent across restarts\n\n---", - "content": "## Guideline\n\nKeyValueStore operations:\n\n- **set**: Store key-value pair\n- **get**: Retrieve value by key\n- **remove**: Delete key\n- **has**: Check if key exists\n- **clear**: Remove all entries\n\nPattern: `KeyValueStore.set(key, value).pipe(...)`\n\n---\n\n## Rationale\n\nWithout persistent storage, transient data is lost:\n\n- **Session data**: Lost on restart\n- **Caches**: Rebuilt from scratch\n- **Configuration**: Hardcoded or file-based\n- **State**: Scattered across code\n\nKeyValueStore enables:\n\n- **Transparent persistence**: Automatic backend handling\n- **Simple API**: Key-value abstraction\n- **Pluggable backends**: Memory, filesystem, database\n- **Effect integration**: Type-safe, composable\n\nReal-world example: Caching API responses\n- **Direct**: Cache in memory Map (lost on restart)\n- **With KeyValueStore**: Persistent across restarts\n\n---\n\n## Good Example\n\nThis example demonstrates storing and retrieving persistent data.\n\n```typescript\nimport { KeyValueStore, Effect } from \"@effect/platform\";\n\ninterface UserSession {\n readonly userId: string;\n readonly token: string;\n readonly expiresAt: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[KEYVALUESTORE] Persistent storage example\\n`);\n\n const store = yield* KeyValueStore.KeyValueStore;\n\n // Example 1: Store session data\n console.log(`[1] Storing session:\\n`);\n\n const session: UserSession = {\n userId: \"user-123\",\n token: \"token-abc-def\",\n expiresAt: Date.now() + 3600000, // 1 hour\n };\n\n yield* store.set(\"session:user-123\", JSON.stringify(session));\n\n yield* Effect.log(`✓ Session stored`);\n\n // Example 2: Retrieve stored data\n console.log(`\\n[2] Retrieving session:\\n`);\n\n const stored = yield* store.get(\"session:user-123\");\n\n if (stored._tag === \"Some\") {\n const retrievedSession = JSON.parse(stored.value) as UserSession;\n\n console.log(` User ID: ${retrievedSession.userId}`);\n console.log(` Token: ${retrievedSession.token}`);\n console.log(\n ` Expires: ${new Date(retrievedSession.expiresAt).toISOString()}`\n );\n }\n\n // Example 3: Check if key exists\n console.log(`\\n[3] Checking keys:\\n`);\n\n const hasSession = yield* store.has(\"session:user-123\");\n const hasOther = yield* store.has(\"session:user-999\");\n\n console.log(` Has session:user-123: ${hasSession}`);\n console.log(` Has session:user-999: ${hasOther}`);\n\n // Example 4: Store multiple cache entries\n console.log(`\\n[4] Caching API responses:\\n`);\n\n const apiResponses = [\n { endpoint: \"/api/users\", data: [{ id: 1, name: \"Alice\" }] },\n { endpoint: \"/api/posts\", data: [{ id: 1, title: \"First Post\" }] },\n { endpoint: \"/api/comments\", data: [] },\n ];\n\n yield* Effect.all(\n apiResponses.map((item) =>\n store.set(\n `cache:${item.endpoint}`,\n JSON.stringify(item.data)\n )\n )\n );\n\n yield* Effect.log(`✓ Cached ${apiResponses.length} endpoints`);\n\n // Example 5: Retrieve cache with expiration\n console.log(`\\n[5] Checking cached data:\\n`);\n\n for (const item of apiResponses) {\n const cached = yield* store.get(`cache:${item.endpoint}`);\n\n if (cached._tag === \"Some\") {\n const data = JSON.parse(cached.value);\n\n console.log(\n ` ${item.endpoint}: ${Array.isArray(data) ? data.length : 1} items`\n );\n }\n }\n\n // Example 6: Remove specific entry\n console.log(`\\n[6] Removing entry:\\n`);\n\n yield* store.remove(\"cache:/api/comments\");\n\n const removed = yield* store.has(\"cache:/api/comments\");\n\n console.log(` Exists after removal: ${removed}`);\n\n // Example 7: Iterate and count entries\n console.log(`\\n[7] Counting entries:\\n`);\n\n const allKeys = yield* store.entries.pipe(\n Effect.map((entries) => entries.length)\n );\n\n console.log(` Total entries: ${allKeys}`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Session Management\n\nImplement session store with expiration:\n\n```typescript\ninterface SessionData {\n readonly userId: string;\n readonly expiresAt: number;\n readonly data: Record;\n}\n\nconst createSessionStore = () =>\n Effect.gen(function* () {\n const store = yield* KeyValueStore.KeyValueStore;\n\n const setSession = (sessionId: string, userId: string, ttlMs: number) =>\n Effect.gen(function* () {\n const session: SessionData = {\n userId,\n expiresAt: Date.now() + ttlMs,\n data: {},\n };\n\n yield* store.set(`session:${sessionId}`, JSON.stringify(session));\n });\n\n const getSession = (sessionId: string) =>\n Effect.gen(function* () {\n const stored = yield* store.get(`session:${sessionId}`);\n\n if (stored._tag === \"None\") {\n return null;\n }\n\n const session = JSON.parse(stored.value) as SessionData;\n\n // Check expiration\n if (Date.now() > session.expiresAt) {\n yield* store.remove(`session:${sessionId}`);\n return null;\n }\n\n return session;\n });\n\n const updateSessionData = (\n sessionId: string,\n key: string,\n value: unknown\n ) =>\n Effect.gen(function* () {\n const session = yield* getSession(sessionId);\n\n if (!session) {\n yield* Effect.fail(new Error(\"Session expired\"));\n }\n\n session!.data[key] = value;\n\n yield* store.set(`session:${sessionId}`, JSON.stringify(session));\n });\n\n return { setSession, getSession, updateSessionData };\n });\n```\n\n---\n\n## Advanced: Tiered Caching\n\nMemory cache with persistent fallback:\n\n```typescript\nconst tieredCache = () =>\n Effect.gen(function* () {\n const store = yield* KeyValueStore.KeyValueStore;\n const memoryCache = new Map();\n\n const get = (key: string) =>\n Effect.gen(function* () {\n // Check memory first\n if (memoryCache.has(key)) {\n yield* Effect.log(`[CACHE] Memory hit: ${key}`);\n return memoryCache.get(key);\n }\n\n // Check persistent store\n const persistent = yield* store.get(key);\n\n if (persistent._tag === \"Some\") {\n const value = JSON.parse(persistent.value);\n\n // Populate memory cache\n memoryCache.set(key, value);\n\n yield* Effect.log(`[CACHE] Store hit: ${key}`);\n\n return value;\n }\n\n yield* Effect.log(`[CACHE] Miss: ${key}`);\n\n return null;\n });\n\n const set = (key: string, value: unknown) =>\n Effect.gen(function* () {\n // Update both caches\n memoryCache.set(key, value);\n\n yield* store.set(key, JSON.stringify(value));\n\n yield* Effect.log(`[CACHE] Set: ${key}`);\n });\n\n const clear = () =>\n Effect.gen(function* () {\n memoryCache.clear();\n yield* store.clear();\n\n yield* Effect.log(`[CACHE] Cleared all`);\n });\n\n return { get, set, clear };\n });\n```\n\n---\n\n## Advanced: Cache with Versioning\n\nTrack and manage cache versions:\n\n```typescript\ninterface CachedItem {\n readonly value: T;\n readonly version: number;\n readonly timestamp: number;\n}\n\nconst versionedCache = (\n key: string,\n version: number\n) =>\n Effect.gen(function* () {\n const store = yield* KeyValueStore.KeyValueStore;\n\n const get = () =>\n Effect.gen(function* () {\n const stored = yield* store.get(`${key}:v${version}`);\n\n if (stored._tag === \"None\") {\n return null;\n }\n\n return JSON.parse(stored.value) as CachedItem;\n });\n\n const set = (value: T) =>\n Effect.gen(function* () {\n const item: CachedItem = {\n value,\n version,\n timestamp: Date.now(),\n };\n\n yield* store.set(`${key}:v${version}`, JSON.stringify(item));\n\n // Clean up old versions\n for (let v = 1; v < version; v++) {\n yield* store.remove(`${key}:v${v}`);\n }\n });\n\n return { get, set };\n });\n\n// Usage: Cache with version management\nconst userCache = versionedCache<{ name: string; email: string }>(\n \"user:123\",\n 2\n);\n```\n\n---\n\n## Advanced: Cache Invalidation Patterns\n\nImplement cache invalidation strategies:\n\n```typescript\nconst cacheWithInvalidation = () =>\n Effect.gen(function* () {\n const store = yield* KeyValueStore.KeyValueStore;\n\n // Tag-based invalidation\n const cacheWithTags = (\n key: string,\n value: unknown,\n tags: string[]\n ) =>\n Effect.gen(function* () {\n yield* store.set(key, JSON.stringify(value));\n\n // Store tag mappings\n for (const tag of tags) {\n const tagged = yield* store.get(`tag:${tag}`);\n\n const keys = tagged._tag === \"Some\" \n ? JSON.parse(tagged.value)\n : [];\n\n if (!keys.includes(key)) {\n keys.push(key);\n }\n\n yield* store.set(`tag:${tag}`, JSON.stringify(keys));\n }\n });\n\n const invalidateByTag = (tag: string) =>\n Effect.gen(function* () {\n const tagged = yield* store.get(`tag:${tag}`);\n\n if (tagged._tag === \"Some\") {\n const keys = JSON.parse(tagged.value) as string[];\n\n yield* Effect.all(keys.map((k) => store.remove(k)));\n\n yield* store.remove(`tag:${tag}`);\n\n yield* Effect.log(\n `[INVALIDATE] Removed ${keys.length} entries for tag: ${tag}`\n );\n }\n });\n\n return { cacheWithTags, invalidateByTag };\n });\n\n// Usage: Invalidate all user caches when user updates\nconst userUpdated = versionedCache(\"user:123\", 1).pipe(\n Effect.flatMap(() =>\n cacheWithInvalidation().pipe(\n Effect.flatMap((cache) => cache.invalidateByTag(\"user-data\"))\n )\n )\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use KeyValueStore when:**\n\n- Simple key-value persistence\n- Session/token storage\n- Caching responses\n- Configuration state\n- Temporary data storage\n- Cross-request data sharing\n\n⚠️ **Trade-offs:**\n\n- Not suitable for complex queries\n- Limited schema validation\n- Performance depends on backend\n- No transactions\n\n---\n\n## Backend Options\n\n| Backend | Persistence | Speed | Scale |\n| --- | --- | --- | --- |\n| **Memory** | No | Very fast | Small |\n| **File** | Yes | Moderate | Medium |\n| **SQLite** | Yes | Good | Medium |\n| **Redis** | Optional | Very fast | Large |\n\n---\n\n## See Also\n\n- [Manage Shared State with Ref](./manage-shared-state-with-ref.mdx) - Memory state\n- [Platform Pattern 2: FileSystem Operations](./platform-filesystem-operations.mdx) - File I/O\n- [Add Caching by Wrapping a Layer](./add-caching-by-wrapping-a-layer.mdx) - Layer caching\n- [Understand Layers for Dependency Injection](./understand-layers-for-dependency-injection.mdx) - Layer patterns" - }, - { - "id": "platform-terminal-interactive", - "title": "Platform Pattern 4: Interactive Terminal I/O", - "description": "Use Terminal for user input/output in CLI applications, providing proper buffering and cross-platform character encoding.", - "skillLevel": "beginner", - "useCases": [ - "platform" - ], - "example": "This example demonstrates building an interactive CLI application.\n\n```typescript\nimport { Terminal, Effect } from \"@effect/platform\";\n\ninterface UserInput {\n readonly name: string;\n readonly email: string;\n readonly age: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[INTERACTIVE CLI] User Information Form\\n`);\n\n // Example 1: Simple prompts\n yield* Terminal.writeLine(`=== User Setup ===`);\n yield* Terminal.writeLine(``);\n\n yield* Terminal.write(`What is your name? `);\n const name = yield* Terminal.readLine();\n\n yield* Terminal.write(`What is your email? `);\n const email = yield* Terminal.readLine();\n\n yield* Terminal.write(`What is your age? `);\n const ageStr = yield* Terminal.readLine();\n\n const age = parseInt(ageStr);\n\n // Example 2: Display collected information\n yield* Terminal.writeLine(``);\n yield* Terminal.writeLine(`=== Summary ===`);\n yield* Terminal.writeLine(`Name: ${name}`);\n yield* Terminal.writeLine(`Email: ${email}`);\n yield* Terminal.writeLine(`Age: ${age}`);\n\n // Example 3: Confirmation\n yield* Terminal.writeLine(``);\n yield* Terminal.write(`Confirm information? (yes/no) `);\n const confirm = yield* Terminal.readLine();\n\n if (confirm.toLowerCase() === \"yes\") {\n yield* Terminal.writeLine(`✓ Information saved`);\n } else {\n yield* Terminal.writeLine(`✗ Cancelled`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Direct stdin/stdout causes issues:\n\n- **No buffering**: Interleaved output in concurrent context\n- **Encoding issues**: Special characters corrupted\n- **Password echo**: Security vulnerability\n- **No type safety**: String manipulation error-prone\n\nTerminal enables:\n\n- **Buffered I/O**: Safe concurrent output\n- **Encoding handling**: UTF-8 and special chars\n- **Password input**: No echo mode\n- **Structured interaction**: Prompts and validation\n\nReal-world example: CLI setup wizard\n- **Direct**: console.log mixed with readline, no error handling\n- **With Terminal**: Structured input, validation, formatted output\n\n---", - "content": "## Guideline\n\nTerminal operations:\n\n- **readLine**: Read single line of user input\n- **readPassword**: Read input without echoing (passwords)\n- **writeLine**: Write line with newline\n- **write**: Write without newline\n- **clearScreen**: Clear terminal\n\nPattern: `Terminal.readLine().pipe(...)`\n\n---\n\n## Rationale\n\nDirect stdin/stdout causes issues:\n\n- **No buffering**: Interleaved output in concurrent context\n- **Encoding issues**: Special characters corrupted\n- **Password echo**: Security vulnerability\n- **No type safety**: String manipulation error-prone\n\nTerminal enables:\n\n- **Buffered I/O**: Safe concurrent output\n- **Encoding handling**: UTF-8 and special chars\n- **Password input**: No echo mode\n- **Structured interaction**: Prompts and validation\n\nReal-world example: CLI setup wizard\n- **Direct**: console.log mixed with readline, no error handling\n- **With Terminal**: Structured input, validation, formatted output\n\n---\n\n## Good Example\n\nThis example demonstrates building an interactive CLI application.\n\n```typescript\nimport { Terminal, Effect } from \"@effect/platform\";\n\ninterface UserInput {\n readonly name: string;\n readonly email: string;\n readonly age: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[INTERACTIVE CLI] User Information Form\\n`);\n\n // Example 1: Simple prompts\n yield* Terminal.writeLine(`=== User Setup ===`);\n yield* Terminal.writeLine(``);\n\n yield* Terminal.write(`What is your name? `);\n const name = yield* Terminal.readLine();\n\n yield* Terminal.write(`What is your email? `);\n const email = yield* Terminal.readLine();\n\n yield* Terminal.write(`What is your age? `);\n const ageStr = yield* Terminal.readLine();\n\n const age = parseInt(ageStr);\n\n // Example 2: Display collected information\n yield* Terminal.writeLine(``);\n yield* Terminal.writeLine(`=== Summary ===`);\n yield* Terminal.writeLine(`Name: ${name}`);\n yield* Terminal.writeLine(`Email: ${email}`);\n yield* Terminal.writeLine(`Age: ${age}`);\n\n // Example 3: Confirmation\n yield* Terminal.writeLine(``);\n yield* Terminal.write(`Confirm information? (yes/no) `);\n const confirm = yield* Terminal.readLine();\n\n if (confirm.toLowerCase() === \"yes\") {\n yield* Terminal.writeLine(`✓ Information saved`);\n } else {\n yield* Terminal.writeLine(`✗ Cancelled`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Menu Selection\n\nBuild interactive menus:\n\n```typescript\ninterface MenuOption {\n readonly label: string;\n readonly value: string;\n}\n\nconst selectFromMenu = (\n prompt: string,\n options: MenuOption[]\n): Effect.Effect =>\n Effect.gen(function* () {\n yield* Terminal.writeLine(`\\n${prompt}:`);\n yield* Terminal.writeLine(``);\n\n options.forEach((option, idx) => {\n console.log(` ${idx + 1}. ${option.label}`);\n });\n\n yield* Terminal.writeLine(``);\n\n while (true) {\n yield* Terminal.write(`Select option (1-${options.length}): `);\n\n const input = yield* Terminal.readLine();\n const selected = parseInt(input) - 1;\n\n if (selected >= 0 && selected < options.length) {\n return options[selected].value;\n }\n\n yield* Terminal.writeLine(`Invalid selection, try again.`);\n }\n });\n\n// Usage\nconst menuDemo = selectFromMenu(\"What would you like to do?\", [\n { label: \"Create new user\", value: \"create\" },\n { label: \"List users\", value: \"list\" },\n { label: \"Exit\", value: \"exit\" },\n]).pipe(\n Effect.tap((selected) =>\n Terminal.writeLine(`You selected: ${selected}`)\n )\n);\n```\n\n---\n\n## Advanced: Progress Display\n\nShow progress during operations:\n\n```typescript\nconst showProgress = (\n total: number,\n interval: number\n): Effect.Effect =>\n Effect.gen(function* () {\n for (let i = 0; i <= total; i++) {\n const percent = ((i / total) * 100).toFixed(0);\n const filled = Math.round((i / total) * 20);\n const empty = 20 - filled;\n const bar = `[${\"=\".repeat(filled)}${\" \".repeat(empty)}]`;\n\n // Clear line and print progress\n yield* Terminal.write(\n `\\r${bar} ${percent}% (${i}/${total})`\n );\n\n if (i < total) {\n yield* Effect.sleep(`${interval} millis`);\n }\n }\n\n yield* Terminal.writeLine(`\\n✓ Complete`);\n });\n\n// Usage: Show progress while processing\nconst processWithProgress = Effect.gen(function* () {\n yield* Terminal.writeLine(`Processing items...`);\n\n const total = 50;\n\n for (let i = 1; i <= total; i++) {\n // Simulate work\n yield* Effect.sleep(\"100 millis\");\n\n // Update progress\n const percent = ((i / total) * 100).toFixed(0);\n yield* Terminal.write(\n `\\rProcessing: ${percent}%`\n );\n }\n\n yield* Terminal.writeLine(`\\n✓ Complete`);\n});\n```\n\n---\n\n## Advanced: Colored Output\n\nFormat terminal output with colors:\n\n```typescript\nenum Color {\n Reset = \"\\x1b[0m\",\n Red = \"\\x1b[31m\",\n Green = \"\\x1b[32m\",\n Yellow = \"\\x1b[33m\",\n Blue = \"\\x1b[34m\",\n Cyan = \"\\x1b[36m\",\n}\n\nconst coloredOutput = (\n text: string,\n color: Color\n): string => `${color}${text}${Color.Reset}`;\n\nconst statusReport = Effect.gen(function* () {\n yield* Terminal.writeLine(coloredOutput(`=== Status Report ===`, Color.Cyan));\n yield* Terminal.writeLine(``);\n\n yield* Terminal.writeLine(\n coloredOutput(`✓ Database connected`, Color.Green)\n );\n\n yield* Terminal.writeLine(\n coloredOutput(`⚠ Cache miss rate high`, Color.Yellow)\n );\n\n yield* Terminal.writeLine(\n coloredOutput(`✗ External API down`, Color.Red)\n );\n\n yield* Terminal.writeLine(``);\n yield* Terminal.writeLine(\n coloredOutput(`Summary: 1 OK, 1 warning, 1 error`, Color.Blue)\n );\n});\n```\n\n---\n\n## Advanced: Table Display\n\nDisplay tabular data:\n\n```typescript\ninterface TableRow {\n readonly [key: string]: string | number;\n}\n\nconst displayTable = (\n headers: string[],\n rows: TableRow[]\n): Effect.Effect =>\n Effect.gen(function* () {\n // Calculate column widths\n const colWidths = headers.map((h, idx) =>\n Math.max(\n h.length,\n Math.max(...rows.map((r) => String(r[h]).length))\n )\n );\n\n // Print header\n const headerLine = headers\n .map((h, idx) => h.padEnd(colWidths[idx]))\n .join(\" | \");\n\n yield* Terminal.writeLine(headerLine);\n yield* Terminal.writeLine(\n \"=\".repeat(headerLine.length)\n );\n\n // Print rows\n for (const row of rows) {\n const rowLine = headers\n .map((h, idx) =>\n String(row[h]).padEnd(colWidths[idx])\n )\n .join(\" | \");\n\n yield* Terminal.writeLine(rowLine);\n }\n });\n\n// Usage\nconst tableDemo = displayTable(\n [\"ID\", \"Name\", \"Status\"],\n [\n { ID: \"1\", Name: \"Alice\", Status: \"Active\" },\n { ID: \"2\", Name: \"Bob\", Status: \"Inactive\" },\n { ID: \"3\", Name: \"Charlie\", Status: \"Active\" },\n ]\n);\n```\n\n---\n\n## Advanced: Validation and Retry\n\nValidate input with retry:\n\n```typescript\nconst readValidated = (\n prompt: string,\n validate: (input: string) => boolean,\n errorMsg: string,\n maxRetries: number = 3\n): Effect.Effect =>\n Effect.gen(function* () {\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n yield* Terminal.write(`${prompt}: `);\n\n const input = yield* Terminal.readLine();\n\n if (validate(input)) {\n return input;\n }\n\n yield* Terminal.writeLine(\n `✗ ${errorMsg} (${maxRetries - attempt} attempts remaining)`\n );\n }\n\n yield* Effect.fail(\n new Error(`Validation failed after ${maxRetries} attempts`)\n );\n });\n\n// Usage: Read valid email\nconst emailInput = readValidated(\n \"Enter email\",\n (input) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(input),\n \"Invalid email format\",\n 3\n);\n\n// Usage: Read positive number\nconst ageInput = readValidated(\n \"Enter age\",\n (input) => {\n const num = parseInt(input);\n return !isNaN(num) && num > 0 && num < 150;\n },\n \"Must be a number between 1 and 149\",\n 3\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use Terminal when:**\n\n- Building interactive CLI applications\n- Collecting user input\n- Displaying formatted output\n- Progress indicators\n- Setup wizards\n- System administration tools\n\n⚠️ **Trade-offs:**\n\n- Blocking on user input\n- Terminal capabilities vary per platform\n- No GUI (text-only)\n- Concurrent I/O can be complex\n\n---\n\n## Cross-Platform Compatibility\n\nDifferent terminals support different features:\n- Colors: Not supported on Windows Console (legacy)\n- Progress: Use carriage return (`\\r`) for updates\n- Unicode: Ensure UTF-8 encoding\n- ANSI codes: May not work on all terminals\n\n---\n\n## See Also\n\n- [Platform Pattern 1: Command Execution](./platform-pattern-command-execution.mdx) - External commands\n- [Handle Side Effects with Effect.sync](./handle-side-effects-with-effect-sync.mdx) - Side effects\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background execution\n- [Use Pipe for Composition](./use-pipe-for-composition.mdx) - Composition patterns" - }, - { - "id": "platform-pattern-path-manipulation", - "title": "Platform Pattern 5: Cross-Platform Path Manipulation", - "description": "Use Effect's platform-aware path utilities to handle separators, absolute/relative paths, and environment variables consistently.", - "skillLevel": "intermediate", - "useCases": [ - "platform" - ], - "example": "This example demonstrates cross-platform path manipulation.\n\n```typescript\nimport { Effect, FileSystem } from \"@effect/platform\";\nimport * as Path from \"node:path\";\nimport * as OS from \"node:os\";\n\ninterface PathOperation {\n readonly input: string;\n readonly description: string;\n}\n\n// Platform info\nconst getPlatformInfo = () =>\n Effect.gen(function* () {\n const platform = process.platform;\n const separator = Path.sep;\n const delimiter = Path.delimiter;\n const homeDir = OS.homedir();\n\n yield* Effect.log(\n `[PLATFORM] OS: ${platform}, Separator: \"${separator}\", Home: ${homeDir}`\n );\n\n return { platform, separator, delimiter, homeDir };\n });\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[PATH MANIPULATION] Cross-platform path operations\\n`);\n\n const platformInfo = yield* getPlatformInfo();\n\n // Example 1: Path joining (handles separators)\n console.log(`\\n[1] Joining paths (handles separators automatically):\\n`);\n\n const segments = [\"data\", \"reports\", \"2024\"];\n\n const joinedPath = Path.join(...segments);\n\n yield* Effect.log(`[JOIN] Input: ${segments.join(\" + \")}`);\n yield* Effect.log(`[JOIN] Output: ${joinedPath}`);\n\n // Example 2: Resolving to absolute paths\n console.log(`\\n[2] Resolving relative → absolute:\\n`);\n\n const relativePath = \"./config/settings.json\";\n\n const absolutePath = Path.resolve(relativePath);\n\n yield* Effect.log(`[RESOLVE] Relative: ${relativePath}`);\n yield* Effect.log(`[RESOLVE] Absolute: ${absolutePath}`);\n\n // Example 3: Path parsing\n console.log(`\\n[3] Parsing path components:\\n`);\n\n const filePath = \"/home/user/documents/report.pdf\";\n\n const parsed = Path.parse(filePath);\n\n yield* Effect.log(`[PARSE] Input: ${filePath}`);\n yield* Effect.log(` root: ${parsed.root}`);\n yield* Effect.log(` dir: ${parsed.dir}`);\n yield* Effect.log(` base: ${parsed.base}`);\n yield* Effect.log(` name: ${parsed.name}`);\n yield* Effect.log(` ext: ${parsed.ext}`);\n\n // Example 4: Environment variable expansion\n console.log(`\\n[4] Environment variable expansion:\\n`);\n\n const expandPath = (pathStr: string): string => {\n let result = pathStr;\n\n // Expand common variables\n result = result.replace(\"$HOME\", OS.homedir());\n result = result.replace(\"~\", OS.homedir());\n result = result.replace(\"$USER\", process.env.USER || \"user\");\n result = result.replace(\"$PWD\", process.cwd());\n\n // Handle Windows-style env vars\n result = result.replace(/%USERPROFILE%/g, OS.homedir());\n result = result.replace(/%USERNAME%/g, process.env.USERNAME || \"user\");\n result = result.replace(/%TEMP%/g, OS.tmpdir());\n\n return result;\n };\n\n const envPaths = [\n \"$HOME/myapp/data\",\n \"~/documents/file.txt\",\n \"$PWD/config\",\n \"/var/log/app.log\",\n ];\n\n for (const envPath of envPaths) {\n const expanded = expandPath(envPath);\n\n yield* Effect.log(\n `[EXPAND] ${envPath} → ${expanded}`\n );\n }\n\n // Example 5: Path normalization (remove redundant separators)\n console.log(`\\n[5] Path normalization:\\n`);\n\n const messyPaths = [\n \"/home//user///documents\",\n \"C:\\\\Users\\\\\\\\documents\\\\\\\\file.txt\",\n \"./config/../config/./settings\",\n \"../data/../../root\",\n ];\n\n for (const messy of messyPaths) {\n const normalized = Path.normalize(messy);\n\n yield* Effect.log(\n `[NORMALIZE] ${messy}`\n );\n yield* Effect.log(\n `[NORMALIZE] → ${normalized}`\n );\n }\n\n // Example 6: Safe path construction with base directory\n console.log(`\\n[6] Safe path construction (path traversal prevention):\\n`);\n\n const baseDir = \"/var/app/data\";\n\n const safeJoin = (base: string, userPath: string): Result => {\n // Reject absolute paths from untrusted input\n if (Path.isAbsolute(userPath)) {\n return { success: false, reason: \"Absolute paths not allowed\" };\n }\n\n // Reject paths with ..\n if (userPath.includes(\"..\")) {\n return { success: false, reason: \"Path traversal attempt detected\" };\n }\n\n // Resolve and verify within base\n const fullPath = Path.resolve(base, userPath);\n\n if (!fullPath.startsWith(base)) {\n return { success: false, reason: \"Path escapes base directory\" };\n }\n\n return { success: true, path: fullPath };\n };\n\n interface Result {\n success: boolean;\n reason?: string;\n path?: T;\n }\n\n const testPaths = [\n \"reports/2024.json\",\n \"/etc/passwd\",\n \"../../../root\",\n \"data/file.txt\",\n ];\n\n for (const test of testPaths) {\n const result = safeJoin(baseDir, test);\n\n if (result.success) {\n yield* Effect.log(`[SAFE] ✓ ${test} → ${result.path}`);\n } else {\n yield* Effect.log(`[SAFE] ✗ ${test} (${result.reason})`);\n }\n }\n\n // Example 7: Relative path calculation\n console.log(`\\n[7] Computing relative paths:\\n`);\n\n const fromDir = \"/home/user/projects/myapp\";\n const toPath = \"/home/user/data/config.json\";\n\n const relativePath2 = Path.relative(fromDir, toPath);\n\n yield* Effect.log(`[RELATIVE] From: ${fromDir}`);\n yield* Effect.log(`[RELATIVE] To: ${toPath}`);\n yield* Effect.log(`[RELATIVE] Relative: ${relativePath2}`);\n\n // Example 8: Common path patterns\n console.log(`\\n[8] Common patterns:\\n`);\n\n // Get file extension\n const fileName = \"document.tar.gz\";\n const ext = Path.extname(fileName);\n const baseName = Path.basename(fileName);\n const dirName = Path.dirname(\"/home/user/file.txt\");\n\n yield* Effect.log(`[PATTERNS] File: ${fileName}`);\n yield* Effect.log(` basename: ${baseName}`);\n yield* Effect.log(` dirname: ${dirName}`);\n yield* Effect.log(` extname: ${ext}`);\n\n // Example 9: Path segments array\n console.log(`\\n[9] Path segments:\\n`);\n\n const segmentPath = \"/home/user/documents/report.pdf\";\n\n const segments2 = segmentPath.split(Path.sep).filter((s) => s);\n\n yield* Effect.log(`[SEGMENTS] ${segmentPath}`);\n yield* Effect.log(`[SEGMENTS] → [${segments2.map((s) => `\"${s}\"`).join(\", \")}]`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "String-based path handling causes problems:\n\n**Problem 1: Platform inconsistency**\n- Write path: `\"C:\\data\\file.txt\"` (Windows)\n- Ship to Linux, gets interpreted as literal \"C:\\data\\file.txt\"\n- File not found errors, production outage\n\n**Problem 2: Path traversal attacks**\n- User supplies path: `\"../../../../etc/passwd\"`\n- No validation → reads sensitive files\n- Security vulnerability\n\n**Problem 3: Environment variable expansion**\n- User's config: `\"$HOME/myapp/data\"`\n- Without expansion: literal `$HOME` in path\n- Can't find files\n\n**Problem 4: Symlink resolution**\n- File at `/etc/ssl/certs/ca-bundle.crt` (symlink)\n- Real file at `/usr/share/ca-certificates/ca-bundle.crt`\n- Both point to same file, but string equality fails\n\nSolutions:\n\n**Platform-aware API**:\n- `path.join()` handles separators\n- `path.resolve()` creates absolute paths\n- `path.parse()` components\n- Auto-handles platform differences\n\n**Variable expansion**:\n- `$HOME`, `~` → user home\n- `$USER` → username\n- `$PWD` → current directory\n\n**Validation**:\n- Reject paths with `..`\n- Reject absolute paths from untrusted input\n- Contain paths within base directory\n\n---", - "content": "## Guideline\n\nPath manipulation requires platform awareness:\n\n- **Separators**: Windows uses `\\`, Unix uses `/`\n- **Absolute vs relative**: `/root` vs `./file`\n- **Environment variables**: `$HOME`, `%APPDATA%`\n- **Resolution**: Normalize, resolve symlinks\n- **Validation**: Prevent path traversal attacks\n\nPattern: Avoid string concatenation, use `path.join()`, `path.resolve()`\n\n---\n\n## Rationale\n\nString-based path handling causes problems:\n\n**Problem 1: Platform inconsistency**\n- Write path: `\"C:\\data\\file.txt\"` (Windows)\n- Ship to Linux, gets interpreted as literal \"C:\\data\\file.txt\"\n- File not found errors, production outage\n\n**Problem 2: Path traversal attacks**\n- User supplies path: `\"../../../../etc/passwd\"`\n- No validation → reads sensitive files\n- Security vulnerability\n\n**Problem 3: Environment variable expansion**\n- User's config: `\"$HOME/myapp/data\"`\n- Without expansion: literal `$HOME` in path\n- Can't find files\n\n**Problem 4: Symlink resolution**\n- File at `/etc/ssl/certs/ca-bundle.crt` (symlink)\n- Real file at `/usr/share/ca-certificates/ca-bundle.crt`\n- Both point to same file, but string equality fails\n\nSolutions:\n\n**Platform-aware API**:\n- `path.join()` handles separators\n- `path.resolve()` creates absolute paths\n- `path.parse()` components\n- Auto-handles platform differences\n\n**Variable expansion**:\n- `$HOME`, `~` → user home\n- `$USER` → username\n- `$PWD` → current directory\n\n**Validation**:\n- Reject paths with `..`\n- Reject absolute paths from untrusted input\n- Contain paths within base directory\n\n---\n\n## Good Example\n\nThis example demonstrates cross-platform path manipulation.\n\n```typescript\nimport { Effect, FileSystem } from \"@effect/platform\";\nimport * as Path from \"node:path\";\nimport * as OS from \"node:os\";\n\ninterface PathOperation {\n readonly input: string;\n readonly description: string;\n}\n\n// Platform info\nconst getPlatformInfo = () =>\n Effect.gen(function* () {\n const platform = process.platform;\n const separator = Path.sep;\n const delimiter = Path.delimiter;\n const homeDir = OS.homedir();\n\n yield* Effect.log(\n `[PLATFORM] OS: ${platform}, Separator: \"${separator}\", Home: ${homeDir}`\n );\n\n return { platform, separator, delimiter, homeDir };\n });\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[PATH MANIPULATION] Cross-platform path operations\\n`);\n\n const platformInfo = yield* getPlatformInfo();\n\n // Example 1: Path joining (handles separators)\n console.log(`\\n[1] Joining paths (handles separators automatically):\\n`);\n\n const segments = [\"data\", \"reports\", \"2024\"];\n\n const joinedPath = Path.join(...segments);\n\n yield* Effect.log(`[JOIN] Input: ${segments.join(\" + \")}`);\n yield* Effect.log(`[JOIN] Output: ${joinedPath}`);\n\n // Example 2: Resolving to absolute paths\n console.log(`\\n[2] Resolving relative → absolute:\\n`);\n\n const relativePath = \"./config/settings.json\";\n\n const absolutePath = Path.resolve(relativePath);\n\n yield* Effect.log(`[RESOLVE] Relative: ${relativePath}`);\n yield* Effect.log(`[RESOLVE] Absolute: ${absolutePath}`);\n\n // Example 3: Path parsing\n console.log(`\\n[3] Parsing path components:\\n`);\n\n const filePath = \"/home/user/documents/report.pdf\";\n\n const parsed = Path.parse(filePath);\n\n yield* Effect.log(`[PARSE] Input: ${filePath}`);\n yield* Effect.log(` root: ${parsed.root}`);\n yield* Effect.log(` dir: ${parsed.dir}`);\n yield* Effect.log(` base: ${parsed.base}`);\n yield* Effect.log(` name: ${parsed.name}`);\n yield* Effect.log(` ext: ${parsed.ext}`);\n\n // Example 4: Environment variable expansion\n console.log(`\\n[4] Environment variable expansion:\\n`);\n\n const expandPath = (pathStr: string): string => {\n let result = pathStr;\n\n // Expand common variables\n result = result.replace(\"$HOME\", OS.homedir());\n result = result.replace(\"~\", OS.homedir());\n result = result.replace(\"$USER\", process.env.USER || \"user\");\n result = result.replace(\"$PWD\", process.cwd());\n\n // Handle Windows-style env vars\n result = result.replace(/%USERPROFILE%/g, OS.homedir());\n result = result.replace(/%USERNAME%/g, process.env.USERNAME || \"user\");\n result = result.replace(/%TEMP%/g, OS.tmpdir());\n\n return result;\n };\n\n const envPaths = [\n \"$HOME/myapp/data\",\n \"~/documents/file.txt\",\n \"$PWD/config\",\n \"/var/log/app.log\",\n ];\n\n for (const envPath of envPaths) {\n const expanded = expandPath(envPath);\n\n yield* Effect.log(\n `[EXPAND] ${envPath} → ${expanded}`\n );\n }\n\n // Example 5: Path normalization (remove redundant separators)\n console.log(`\\n[5] Path normalization:\\n`);\n\n const messyPaths = [\n \"/home//user///documents\",\n \"C:\\\\Users\\\\\\\\documents\\\\\\\\file.txt\",\n \"./config/../config/./settings\",\n \"../data/../../root\",\n ];\n\n for (const messy of messyPaths) {\n const normalized = Path.normalize(messy);\n\n yield* Effect.log(\n `[NORMALIZE] ${messy}`\n );\n yield* Effect.log(\n `[NORMALIZE] → ${normalized}`\n );\n }\n\n // Example 6: Safe path construction with base directory\n console.log(`\\n[6] Safe path construction (path traversal prevention):\\n`);\n\n const baseDir = \"/var/app/data\";\n\n const safeJoin = (base: string, userPath: string): Result => {\n // Reject absolute paths from untrusted input\n if (Path.isAbsolute(userPath)) {\n return { success: false, reason: \"Absolute paths not allowed\" };\n }\n\n // Reject paths with ..\n if (userPath.includes(\"..\")) {\n return { success: false, reason: \"Path traversal attempt detected\" };\n }\n\n // Resolve and verify within base\n const fullPath = Path.resolve(base, userPath);\n\n if (!fullPath.startsWith(base)) {\n return { success: false, reason: \"Path escapes base directory\" };\n }\n\n return { success: true, path: fullPath };\n };\n\n interface Result {\n success: boolean;\n reason?: string;\n path?: T;\n }\n\n const testPaths = [\n \"reports/2024.json\",\n \"/etc/passwd\",\n \"../../../root\",\n \"data/file.txt\",\n ];\n\n for (const test of testPaths) {\n const result = safeJoin(baseDir, test);\n\n if (result.success) {\n yield* Effect.log(`[SAFE] ✓ ${test} → ${result.path}`);\n } else {\n yield* Effect.log(`[SAFE] ✗ ${test} (${result.reason})`);\n }\n }\n\n // Example 7: Relative path calculation\n console.log(`\\n[7] Computing relative paths:\\n`);\n\n const fromDir = \"/home/user/projects/myapp\";\n const toPath = \"/home/user/data/config.json\";\n\n const relativePath2 = Path.relative(fromDir, toPath);\n\n yield* Effect.log(`[RELATIVE] From: ${fromDir}`);\n yield* Effect.log(`[RELATIVE] To: ${toPath}`);\n yield* Effect.log(`[RELATIVE] Relative: ${relativePath2}`);\n\n // Example 8: Common path patterns\n console.log(`\\n[8] Common patterns:\\n`);\n\n // Get file extension\n const fileName = \"document.tar.gz\";\n const ext = Path.extname(fileName);\n const baseName = Path.basename(fileName);\n const dirName = Path.dirname(\"/home/user/file.txt\");\n\n yield* Effect.log(`[PATTERNS] File: ${fileName}`);\n yield* Effect.log(` basename: ${baseName}`);\n yield* Effect.log(` dirname: ${dirName}`);\n yield* Effect.log(` extname: ${ext}`);\n\n // Example 9: Path segments array\n console.log(`\\n[9] Path segments:\\n`);\n\n const segmentPath = \"/home/user/documents/report.pdf\";\n\n const segments2 = segmentPath.split(Path.sep).filter((s) => s);\n\n yield* Effect.log(`[SEGMENTS] ${segmentPath}`);\n yield* Effect.log(`[SEGMENTS] → [${segments2.map((s) => `\"${s}\"`).join(\", \")}]`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Path Configuration Management\n\nBuild config with environment-aware paths:\n\n```typescript\ninterface AppConfig {\n dataDir: string;\n logsDir: string;\n configFile: string;\n cacheDir: string;\n}\n\nconst resolveAppConfig = (): Effect.Effect =>\n Effect.gen(function* () {\n const baseDir = process.env.APP_HOME ||\n Path.join(OS.homedir(), \".myapp\");\n\n return {\n dataDir: Path.join(baseDir, \"data\"),\n logsDir: Path.join(baseDir, \"logs\"),\n configFile: Path.join(baseDir, \"config\", \"settings.json\"),\n cacheDir: Path.join(OS.tmpdir(), \"myapp-cache\"),\n };\n });\n\n// Usage\nconst config = resolveAppConfig();\n```\n\n---\n\n## Advanced: Glob Pattern Matching\n\nFind files matching patterns:\n\n```typescript\nconst findFilesMatching = (baseDir: string, pattern: string) =>\n Effect.gen(function* () {\n // Pattern examples:\n // \"*.txt\" - all .txt files\n // \"**/*.ts\" - all .ts files recursively\n // \"src/**/test/**\" - nested pattern\n\n // In real code, use 'glob' package\n yield* Effect.log(`[GLOB] Pattern: ${pattern}`);\n yield* Effect.log(`[GLOB] Base: ${baseDir}`);\n\n // Would return matching paths\n return [\n Path.join(baseDir, \"file1.txt\"),\n Path.join(baseDir, \"file2.txt\"),\n ];\n });\n```\n\n---\n\n## Advanced: Safe Symbolic Link Resolution\n\nHandle symlinks safely:\n\n```typescript\nconst resolvePath = (input: string) =>\n Effect.gen(function* () {\n // Normalize path\n let normalized = Path.normalize(input);\n\n // Expand environment variables\n normalized = normalized.replace(\"$HOME\", OS.homedir());\n normalized = normalized.replace(\"~\", OS.homedir());\n\n // Resolve to absolute\n const absolute = Path.resolve(normalized);\n\n // Check for symlinks (in real code, use fs.readlink)\n const isSymlink = false; // Placeholder\n\n if (isSymlink) {\n yield* Effect.log(\n `[SYMLINK] ${input} → ${absolute} (symlink detected)`\n );\n }\n\n return absolute;\n });\n```\n\n---\n\n## Platform Differences\n\n| Aspect | Windows | Unix/Linux | macOS |\n| --- | --- | --- | --- |\n| **Separator** | `\\` | `/` | `/` |\n| **Absolute** | `C:\\` | `/` | `/` |\n| **Home** | `C:\\Users\\user` | `/home/user` | `/Users/user` |\n| **Env var** | `%VAR%` | `$VAR` | `$VAR` |\n| **Temp** | `%TEMP%` | `/tmp` | `/var/tmp` |\n\n---\n\n## When to Use This Pattern\n\n✅ **Use platform-aware paths when:**\n- Cross-platform applications\n- Config file locations\n- User data directories\n- Build output paths\n- Log file locations\n\n✅ **Use safe joining when:**\n- User-supplied paths\n- Config-based paths\n- Preventing traversal\n- Security-sensitive\n\n⚠️ **Trade-offs:**\n- Platform abstraction cost\n- Environment-specific behavior\n- Testing complexity\n- Debugging symlink issues\n\n---\n\n## Path Validation Checklist\n\n- ✅ Reject absolute paths from untrusted input\n- ✅ Reject paths with `..` components\n- ✅ Verify paths stay within base directory\n- ✅ Normalize paths before validation\n- ✅ Log suspicious paths for audit\n- ✅ Handle symlinks safely\n- ✅ Test on all target platforms\n- ✅ Document path requirements\n\n---\n\n## Common Mistakes\n\n❌ String concatenation: `\"/home/user/\" + userPath`\n✅ Use `Path.join()`: `Path.join(\"/home/user\", userPath)`\n\n❌ Hardcoded separators: `\"data\\\\file.txt\"`\n✅ Use `Path.join()`: `Path.join(\"data\", \"file.txt\")`\n\n❌ No variable expansion: `\"$HOME/data\"`\n✅ Expand variables: `\"$HOME/data\".replace(\"$HOME\", os.homedir())`\n\n❌ Trust user paths: `Path.resolve(userInput)`\n✅ Validate first: `safeJoin(baseDir, userInput)`\n\n---\n\n## See Also\n\n- [Platform Pattern 2: FileSystem Operations](./platform-filesystem-operations.mdx) - File I/O patterns\n- [Platform Pattern 4: Terminal I/O](./platform-terminal-interactive.mdx) - User input patterns\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error safety\n- [Error Handling Pattern 3: Custom Strategies](./error-handling-pattern-custom-strategies.mdx) - Validation errors" - }, - { - "id": "platform-pattern-advanced-filesystem", - "title": "Platform Pattern 6: Advanced FileSystem Operations", - "description": "Use advanced file system patterns to implement efficient, reliable file operations with proper error handling and resource cleanup.", - "skillLevel": "advanced", - "useCases": [ - "platform" - ], - "example": "This example demonstrates advanced file system patterns.\n\n```typescript\nimport { Effect, Stream, Ref, FileSystem } from \"@effect/platform\";\nimport * as Path from \"node:path\";\nimport * as FS from \"node:fs\";\nimport * as PromiseFS from \"node:fs/promises\";\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED FILESYSTEM] Complex file operations\\n`);\n\n // Example 1: Atomic file write with temporary file\n console.log(`[1] Atomic write (crash-safe):\\n`);\n\n const atomicWrite = (\n filePath: string,\n content: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n const tempPath = `${filePath}.tmp`;\n\n try {\n // Step 1: Write to temporary file\n yield* Effect.promise(() =>\n PromiseFS.writeFile(tempPath, content, \"utf-8\")\n );\n\n yield* Effect.log(`[WRITE] Wrote to temporary file`);\n\n // Step 2: Ensure on disk (fsync)\n yield* Effect.promise(() =>\n PromiseFS.writeFile(tempPath, content, \"utf-8\")\n );\n\n yield* Effect.log(`[FSYNC] Data on disk`);\n\n // Step 3: Atomic rename\n yield* Effect.promise(() =>\n PromiseFS.rename(tempPath, filePath)\n );\n\n yield* Effect.log(`[RENAME] Atomic rename complete`);\n } catch (error) {\n // Cleanup on failure\n try {\n yield* Effect.promise(() => PromiseFS.unlink(tempPath));\n } catch {\n // Ignore cleanup errors\n }\n\n yield* Effect.fail(error);\n }\n });\n\n // Test atomic write\n const testFile = \"./test-file.txt\";\n\n yield* atomicWrite(testFile, \"Important configuration\\n\");\n\n // Verify file\n const content = yield* Effect.promise(() =>\n PromiseFS.readFile(testFile, \"utf-8\")\n );\n\n yield* Effect.log(`[READ] Got: \"${content.trim()}\"\\n`);\n\n // Example 2: Streaming read (memory efficient)\n console.log(`[2] Streaming read (handle large files):\\n`);\n\n const streamingRead = (filePath: string) =>\n Effect.gen(function* () {\n let byteCount = 0;\n let lineCount = 0;\n\n const readStream = FS.createReadStream(filePath, {\n encoding: \"utf-8\",\n highWaterMark: 64 * 1024, // 64KB chunks\n });\n\n yield* Effect.log(`[STREAM] Starting read with 64KB chunks`);\n\n const processLine = (line: string) =>\n Effect.gen(function* () {\n byteCount += line.length;\n lineCount++;\n\n if (lineCount <= 2 || lineCount % 1000 === 0) {\n yield* Effect.log(\n `[LINE ${lineCount}] Length: ${line.length} bytes`\n );\n }\n });\n\n // In real code, process all lines\n yield* processLine(\"line 1\");\n yield* processLine(\"line 2\");\n\n yield* Effect.log(\n `[TOTAL] Read ${lineCount} lines, ${byteCount} bytes`\n );\n });\n\n yield* streamingRead(testFile);\n\n // Example 3: Recursive directory listing\n console.log(`\\n[3] Recursive directory traversal:\\n`);\n\n const recursiveList = (\n dir: string,\n maxDepth: number = 3\n ): Effect.Effect> =>\n Effect.gen(function* () {\n const results: Array<{ path: string; type: \"file\" | \"dir\" }> = [];\n\n const traverse = (currentDir: string, depth: number) =>\n Effect.gen(function* () {\n if (depth > maxDepth) {\n return;\n }\n\n const entries = yield* Effect.promise(() =>\n PromiseFS.readdir(currentDir, { withFileTypes: true })\n );\n\n for (const entry of entries) {\n const fullPath = Path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n results.push({ path: fullPath, type: \"dir\" });\n\n yield* traverse(fullPath, depth + 1);\n } else {\n results.push({ path: fullPath, type: \"file\" });\n }\n }\n });\n\n yield* traverse(dir, 0);\n\n return results;\n });\n\n // List files in current directory\n const entries = yield* recursiveList(\".\", 1);\n\n yield* Effect.log(\n `[ENTRIES] Found ${entries.length} items:`\n );\n\n for (const entry of entries.slice(0, 5)) {\n const type = entry.type === \"file\" ? \"📄\" : \"📁\";\n\n yield* Effect.log(` ${type} ${entry.path}`);\n }\n\n // Example 4: Bulk file operations\n console.log(`\\n[4] Bulk operations (efficient batching):\\n`);\n\n const bulkCreate = (files: Array<{ name: string; content: string }>) =>\n Effect.gen(function* () {\n yield* Effect.log(`[BULK] Creating ${files.length} files...`);\n\n for (const file of files) {\n yield* atomicWrite(`./${file.name}`, file.content);\n }\n\n yield* Effect.log(`[BULK] Created ${files.length} files`);\n });\n\n const testFiles = [\n { name: \"config1.txt\", content: \"Config 1\" },\n { name: \"config2.txt\", content: \"Config 2\" },\n { name: \"config3.txt\", content: \"Config 3\" },\n ];\n\n yield* bulkCreate(testFiles);\n\n // Example 5: File watching (detect changes)\n console.log(`\\n[5] File watching (react to changes):\\n`);\n\n const watchFile = (filePath: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[WATCH] Starting to watch: ${filePath}`);\n\n let changeCount = 0;\n\n // Simulate file watcher\n const checkForChanges = () =>\n Effect.gen(function* () {\n for (let i = 0; i < 3; i++) {\n yield* Effect.sleep(\"100 millis\");\n\n // Check file modification time\n const stat = yield* Effect.promise(() =>\n PromiseFS.stat(filePath)\n );\n\n // In real implementation, compare previous mtime\n if (i === 1) {\n changeCount++;\n\n yield* Effect.log(\n `[CHANGE] File modified (${stat.size} bytes)`\n );\n }\n }\n });\n\n yield* checkForChanges();\n\n yield* Effect.log(`[WATCH] Detected ${changeCount} changes`);\n });\n\n yield* watchFile(testFile);\n\n // Example 6: Safe concurrent file operations\n console.log(`\\n[6] Concurrent file operations with safety:\\n`);\n\n const lockFile = (filePath: string) =>\n Effect.gen(function* () {\n const lockPath = `${filePath}.lock`;\n\n // Acquire lock\n yield* atomicWrite(lockPath, \"locked\");\n\n yield* Effect.log(`[LOCK] Acquired: ${lockPath}`);\n\n try {\n // Critical section\n yield* Effect.sleep(\"50 millis\");\n\n yield* Effect.log(`[CRITICAL] Operating on locked file`);\n } finally {\n // Release lock\n yield* Effect.promise(() =>\n PromiseFS.unlink(lockPath)\n );\n\n yield* Effect.log(`[UNLOCK] Released: ${lockPath}`);\n }\n });\n\n yield* lockFile(testFile);\n\n // Example 7: Efficient file copying\n console.log(`\\n[7] Efficient file copying:\\n`);\n\n const efficientCopy = (\n source: string,\n destination: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n const stat = yield* Effect.promise(() =>\n PromiseFS.stat(source)\n );\n\n yield* Effect.log(\n `[COPY] Reading ${(stat.size / 1024).toFixed(2)}KB`\n );\n\n const content = yield* Effect.promise(() =>\n PromiseFS.readFile(source)\n );\n\n yield* atomicWrite(destination, content.toString());\n\n yield* Effect.log(`[COPY] Complete: ${destination}`);\n });\n\n yield* efficientCopy(testFile, \"./test-file-copy.txt\");\n\n // Cleanup\n yield* Effect.log(`\\n[CLEANUP] Removing test files`);\n\n for (const name of [testFile, \"test-file-copy.txt\", ...testFiles.map((f) => `./${f.name}`)]) {\n try {\n yield* Effect.promise(() =>\n PromiseFS.unlink(name)\n );\n\n yield* Effect.log(`[REMOVED] ${name}`);\n } catch {\n // File doesn't exist, that's ok\n }\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Simple file operations cause problems at scale:\n\n**Problem 1: Corrupted files**\n- Write config file\n- Server crashes mid-write\n- File is partial/corrupted\n- Application fails to start\n- Production outage\n\n**Problem 2: Large file handling**\n- Load 10GB file into memory\n- Server runs out of memory\n- Everything crashes\n- Now handling outages instead of serving\n\n**Problem 3: Directory synchronization**\n- Copy directory tree\n- Process interrupted\n- Some files copied, some not\n- Directory in inconsistent state\n- Hard to recover\n\n**Problem 4: Inefficient updates**\n- Update 10,000 files one by one\n- Each file system call is slow\n- Takes hours\n- Meanwhile, users can't access data\n\n**Problem 5: File locking**\n- Process A reads file\n- Process B writes file\n- Process A gets partially written file\n- Data corruption\n\nSolutions:\n\n**Atomic writes**:\n- Write to temporary file\n- Fsync (guarantee on disk)\n- Atomic rename\n- No corruption even on crash\n\n**Streaming**:\n- Process large files in chunks\n- Keep memory constant\n- Efficient for any file size\n\n**Bulk operations**:\n- Batch multiple operations\n- Reduce system calls\n- Faster overall completion\n\n**File watching**:\n- React to changes\n- Avoid polling\n- Real-time responsiveness\n\n---", - "content": "## Guideline\n\nAdvanced file system operations require careful handling:\n\n- **Atomic writes**: Prevent partial file corruption\n- **File watching**: React to file changes\n- **Recursive operations**: Handle directory trees\n- **Bulk operations**: Efficient batch processing\n- **Streaming**: Handle large files without loading all in memory\n- **Permissions**: Handle access control safely\n\nPattern: Combine `FileSystem` API with `Ref` for state, `Stream` for data\n\n---\n\n## Rationale\n\nSimple file operations cause problems at scale:\n\n**Problem 1: Corrupted files**\n- Write config file\n- Server crashes mid-write\n- File is partial/corrupted\n- Application fails to start\n- Production outage\n\n**Problem 2: Large file handling**\n- Load 10GB file into memory\n- Server runs out of memory\n- Everything crashes\n- Now handling outages instead of serving\n\n**Problem 3: Directory synchronization**\n- Copy directory tree\n- Process interrupted\n- Some files copied, some not\n- Directory in inconsistent state\n- Hard to recover\n\n**Problem 4: Inefficient updates**\n- Update 10,000 files one by one\n- Each file system call is slow\n- Takes hours\n- Meanwhile, users can't access data\n\n**Problem 5: File locking**\n- Process A reads file\n- Process B writes file\n- Process A gets partially written file\n- Data corruption\n\nSolutions:\n\n**Atomic writes**:\n- Write to temporary file\n- Fsync (guarantee on disk)\n- Atomic rename\n- No corruption even on crash\n\n**Streaming**:\n- Process large files in chunks\n- Keep memory constant\n- Efficient for any file size\n\n**Bulk operations**:\n- Batch multiple operations\n- Reduce system calls\n- Faster overall completion\n\n**File watching**:\n- React to changes\n- Avoid polling\n- Real-time responsiveness\n\n---\n\n## Good Example\n\nThis example demonstrates advanced file system patterns.\n\n```typescript\nimport { Effect, Stream, Ref, FileSystem } from \"@effect/platform\";\nimport * as Path from \"node:path\";\nimport * as FS from \"node:fs\";\nimport * as PromiseFS from \"node:fs/promises\";\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED FILESYSTEM] Complex file operations\\n`);\n\n // Example 1: Atomic file write with temporary file\n console.log(`[1] Atomic write (crash-safe):\\n`);\n\n const atomicWrite = (\n filePath: string,\n content: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n const tempPath = `${filePath}.tmp`;\n\n try {\n // Step 1: Write to temporary file\n yield* Effect.promise(() =>\n PromiseFS.writeFile(tempPath, content, \"utf-8\")\n );\n\n yield* Effect.log(`[WRITE] Wrote to temporary file`);\n\n // Step 2: Ensure on disk (fsync)\n yield* Effect.promise(() =>\n PromiseFS.writeFile(tempPath, content, \"utf-8\")\n );\n\n yield* Effect.log(`[FSYNC] Data on disk`);\n\n // Step 3: Atomic rename\n yield* Effect.promise(() =>\n PromiseFS.rename(tempPath, filePath)\n );\n\n yield* Effect.log(`[RENAME] Atomic rename complete`);\n } catch (error) {\n // Cleanup on failure\n try {\n yield* Effect.promise(() => PromiseFS.unlink(tempPath));\n } catch {\n // Ignore cleanup errors\n }\n\n yield* Effect.fail(error);\n }\n });\n\n // Test atomic write\n const testFile = \"./test-file.txt\";\n\n yield* atomicWrite(testFile, \"Important configuration\\n\");\n\n // Verify file\n const content = yield* Effect.promise(() =>\n PromiseFS.readFile(testFile, \"utf-8\")\n );\n\n yield* Effect.log(`[READ] Got: \"${content.trim()}\"\\n`);\n\n // Example 2: Streaming read (memory efficient)\n console.log(`[2] Streaming read (handle large files):\\n`);\n\n const streamingRead = (filePath: string) =>\n Effect.gen(function* () {\n let byteCount = 0;\n let lineCount = 0;\n\n const readStream = FS.createReadStream(filePath, {\n encoding: \"utf-8\",\n highWaterMark: 64 * 1024, // 64KB chunks\n });\n\n yield* Effect.log(`[STREAM] Starting read with 64KB chunks`);\n\n const processLine = (line: string) =>\n Effect.gen(function* () {\n byteCount += line.length;\n lineCount++;\n\n if (lineCount <= 2 || lineCount % 1000 === 0) {\n yield* Effect.log(\n `[LINE ${lineCount}] Length: ${line.length} bytes`\n );\n }\n });\n\n // In real code, process all lines\n yield* processLine(\"line 1\");\n yield* processLine(\"line 2\");\n\n yield* Effect.log(\n `[TOTAL] Read ${lineCount} lines, ${byteCount} bytes`\n );\n });\n\n yield* streamingRead(testFile);\n\n // Example 3: Recursive directory listing\n console.log(`\\n[3] Recursive directory traversal:\\n`);\n\n const recursiveList = (\n dir: string,\n maxDepth: number = 3\n ): Effect.Effect> =>\n Effect.gen(function* () {\n const results: Array<{ path: string; type: \"file\" | \"dir\" }> = [];\n\n const traverse = (currentDir: string, depth: number) =>\n Effect.gen(function* () {\n if (depth > maxDepth) {\n return;\n }\n\n const entries = yield* Effect.promise(() =>\n PromiseFS.readdir(currentDir, { withFileTypes: true })\n );\n\n for (const entry of entries) {\n const fullPath = Path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n results.push({ path: fullPath, type: \"dir\" });\n\n yield* traverse(fullPath, depth + 1);\n } else {\n results.push({ path: fullPath, type: \"file\" });\n }\n }\n });\n\n yield* traverse(dir, 0);\n\n return results;\n });\n\n // List files in current directory\n const entries = yield* recursiveList(\".\", 1);\n\n yield* Effect.log(\n `[ENTRIES] Found ${entries.length} items:`\n );\n\n for (const entry of entries.slice(0, 5)) {\n const type = entry.type === \"file\" ? \"📄\" : \"📁\";\n\n yield* Effect.log(` ${type} ${entry.path}`);\n }\n\n // Example 4: Bulk file operations\n console.log(`\\n[4] Bulk operations (efficient batching):\\n`);\n\n const bulkCreate = (files: Array<{ name: string; content: string }>) =>\n Effect.gen(function* () {\n yield* Effect.log(`[BULK] Creating ${files.length} files...`);\n\n for (const file of files) {\n yield* atomicWrite(`./${file.name}`, file.content);\n }\n\n yield* Effect.log(`[BULK] Created ${files.length} files`);\n });\n\n const testFiles = [\n { name: \"config1.txt\", content: \"Config 1\" },\n { name: \"config2.txt\", content: \"Config 2\" },\n { name: \"config3.txt\", content: \"Config 3\" },\n ];\n\n yield* bulkCreate(testFiles);\n\n // Example 5: File watching (detect changes)\n console.log(`\\n[5] File watching (react to changes):\\n`);\n\n const watchFile = (filePath: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[WATCH] Starting to watch: ${filePath}`);\n\n let changeCount = 0;\n\n // Simulate file watcher\n const checkForChanges = () =>\n Effect.gen(function* () {\n for (let i = 0; i < 3; i++) {\n yield* Effect.sleep(\"100 millis\");\n\n // Check file modification time\n const stat = yield* Effect.promise(() =>\n PromiseFS.stat(filePath)\n );\n\n // In real implementation, compare previous mtime\n if (i === 1) {\n changeCount++;\n\n yield* Effect.log(\n `[CHANGE] File modified (${stat.size} bytes)`\n );\n }\n }\n });\n\n yield* checkForChanges();\n\n yield* Effect.log(`[WATCH] Detected ${changeCount} changes`);\n });\n\n yield* watchFile(testFile);\n\n // Example 6: Safe concurrent file operations\n console.log(`\\n[6] Concurrent file operations with safety:\\n`);\n\n const lockFile = (filePath: string) =>\n Effect.gen(function* () {\n const lockPath = `${filePath}.lock`;\n\n // Acquire lock\n yield* atomicWrite(lockPath, \"locked\");\n\n yield* Effect.log(`[LOCK] Acquired: ${lockPath}`);\n\n try {\n // Critical section\n yield* Effect.sleep(\"50 millis\");\n\n yield* Effect.log(`[CRITICAL] Operating on locked file`);\n } finally {\n // Release lock\n yield* Effect.promise(() =>\n PromiseFS.unlink(lockPath)\n );\n\n yield* Effect.log(`[UNLOCK] Released: ${lockPath}`);\n }\n });\n\n yield* lockFile(testFile);\n\n // Example 7: Efficient file copying\n console.log(`\\n[7] Efficient file copying:\\n`);\n\n const efficientCopy = (\n source: string,\n destination: string\n ): Effect.Effect =>\n Effect.gen(function* () {\n const stat = yield* Effect.promise(() =>\n PromiseFS.stat(source)\n );\n\n yield* Effect.log(\n `[COPY] Reading ${(stat.size / 1024).toFixed(2)}KB`\n );\n\n const content = yield* Effect.promise(() =>\n PromiseFS.readFile(source)\n );\n\n yield* atomicWrite(destination, content.toString());\n\n yield* Effect.log(`[COPY] Complete: ${destination}`);\n });\n\n yield* efficientCopy(testFile, \"./test-file-copy.txt\");\n\n // Cleanup\n yield* Effect.log(`\\n[CLEANUP] Removing test files`);\n\n for (const name of [testFile, \"test-file-copy.txt\", ...testFiles.map((f) => `./${f.name}`)]) {\n try {\n yield* Effect.promise(() =>\n PromiseFS.unlink(name)\n );\n\n yield* Effect.log(`[REMOVED] ${name}`);\n } catch {\n // File doesn't exist, that's ok\n }\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Batch Processing with Progress\n\nTrack progress across large operations:\n\n```typescript\ninterface ProgressTracker {\n total: number;\n completed: number;\n failed: number;\n startTime: Date;\n}\n\nconst processBulkFiles = (\n files: string[],\n processor: (file: string) => Effect.Effect\n) =>\n Effect.gen(function* () {\n const progress = yield* Ref.make({\n total: files.length,\n completed: 0,\n failed: 0,\n startTime: new Date(),\n });\n\n for (const file of files) {\n yield* processor(file).pipe(\n Effect.tap(() =>\n Ref.modify(progress, (p) => [\n undefined,\n { ...p, completed: p.completed + 1 },\n ])\n ),\n Effect.catchAll((error) =>\n Ref.modify(progress, (p) => [\n undefined,\n { ...p, failed: p.failed + 1 },\n ])\n )\n );\n }\n\n const final = yield* Ref.get(progress);\n const elapsed = Date.now() - final.startTime.getTime();\n\n yield* Effect.log(\n `[PROGRESS] Completed: ${final.completed}/${final.total}, Failed: ${final.failed}, Time: ${elapsed}ms`\n );\n });\n```\n\n---\n\n## Advanced: Transactional Directory Updates\n\nAtomic directory-level operations:\n\n```typescript\nconst transactionalCopyDir = (\n source: string,\n destination: string\n) =>\n Effect.gen(function* () {\n const tempDest = `${destination}.tmp`;\n\n try {\n // Create in temporary location\n yield* Effect.promise(() =>\n PromiseFS.mkdir(tempDest, { recursive: true })\n );\n\n // Copy all files\n const files = yield* Effect.promise(() =>\n PromiseFS.readdir(source)\n );\n\n for (const file of files) {\n const srcPath = Path.join(source, file);\n const dstPath = Path.join(tempDest, file);\n\n yield* Effect.promise(() =>\n PromiseFS.copyFile(srcPath, dstPath)\n );\n }\n\n // Atomic rename\n yield* Effect.promise(() =>\n PromiseFS.rename(tempDest, destination)\n );\n } catch (error) {\n // Rollback\n try {\n yield* Effect.promise(() =>\n PromiseFS.rm(tempDest, { recursive: true })\n );\n } catch {\n // Ignore\n }\n\n yield* Effect.fail(error);\n }\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use atomic writes when:**\n- Config files\n- Database files\n- Any file used by production\n- Risk of corruption unacceptable\n\n✅ **Use streaming when:**\n- Large files (>100MB)\n- Memory-constrained\n- Real-time processing\n- Log files\n\n✅ **Use file watching when:**\n- Config hot-reload\n- Auto-restart on code changes\n- Dev tooling\n- Monitoring\n\n⚠️ **Trade-offs:**\n- More complexity\n- Temporary files overhead\n- Latency for atomicity\n- Platform differences\n\n---\n\n## Performance Tips\n\n| Operation | Strategy | Benefit |\n| --- | --- | --- |\n| **Large files** | Streaming | Constant memory |\n| **Bulk creates** | Batching | Fewer syscalls |\n| **Safety** | Atomic rename | Crash-safe |\n| **Concurrency** | File locks | Prevent corruption |\n| **Efficiency** | Buffering | Better throughput |\n\n---\n\n## See Also\n\n- [Platform Pattern 2: FileSystem Operations](./platform-filesystem-operations.mdx) - Basic file I/O\n- [Platform Pattern 5: Path Manipulation](./platform-pattern-path-manipulation.mdx) - Path handling\n- [Stream Pattern 6: Resource Management](./stream-pattern-resource-management.mdx) - Resource cleanup\n- [Error Handling Pattern 1: Accumulation](./error-handling-pattern-accumulation.mdx) - Error collection" - }, - { - "id": "poll-for-status-until-task-completes", - "title": "Poll for Status Until a Task Completes", - "description": "Use Effect.race to run a repeating polling task that is automatically interrupted when a main task completes.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This program simulates a long-running data processing job. While it's running, a separate effect polls for its status every 2 seconds. When the main job finishes after 10 seconds, the polling automatically stops.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\n// The main task that takes a long time to complete\nconst longRunningJob = Effect.log(\"Data processing complete!\").pipe(\n Effect.delay(Duration.seconds(10))\n);\n\n// The polling task that checks the status\nconst pollStatus = Effect.log(\"Polling for job status: In Progress...\");\n\n// A schedule that repeats the polling task every 2 seconds, forever\nconst pollingSchedule = Schedule.fixed(Duration.seconds(2));\n\n// The complete polling effect that will run indefinitely until interrupted\nconst repeatingPoller = pollStatus.pipe(Effect.repeat(pollingSchedule));\n\n// Race the main job against the poller.\n// The longRunningJob will win after 10 seconds, interrupting the poller.\nconst program = Effect.race(longRunningJob, repeatingPoller);\n\nEffect.runPromise(program);\n/*\nOutput:\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nData processing complete!\n*/\n```\n\n---", - "antiPattern": "Manually managing the lifecycle of the polling fiber. This is more verbose, imperative, and error-prone. You have to remember to interrupt the polling fiber in all possible exit paths (success, failure, etc.), which `Effect.race` does for you automatically.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\nimport { longRunningJob, repeatingPoller } from \"./somewhere\";\n\n// ❌ WRONG: Manual fiber management is complex.\nconst program = Effect.gen(function* () {\n // Manually fork the poller into the background\n const pollerFiber = yield* Effect.fork(repeatingPoller);\n\n try {\n // Run the main job\n const result = yield* longRunningJob;\n return result;\n } finally {\n // You MUST remember to interrupt the poller when you're done.\n yield* Fiber.interrupt(pollerFiber);\n }\n});\n```", - "explanation": "This pattern elegantly solves the problem of coordinating a long-running job with a status-checking mechanism. Instead of manually managing fibers with `fork` and `interrupt`, you can declare this relationship with `Effect.race`.\n\nThe key is that the polling effect is set up to repeat on a schedule that runs indefinitely (or for a very long time). Because it never completes on its own, it can never \"win\" the race. The main task is the only one that can complete successfully. When it does, it wins the race, and Effect's structured concurrency guarantees that the losing effect (the poller) is safely interrupted.\n\nThis creates a self-contained, declarative, and leak-free unit of work.\n\n---", - "content": "## Guideline\n\nTo run a periodic task (a \"poller\") that should only run for the duration of another main task, combine them using `Effect.race`. The main task will \"win\" the race upon completion, which automatically interrupts and cleans up the repeating polling effect.\n\n---\n\n## Rationale\n\nThis pattern elegantly solves the problem of coordinating a long-running job with a status-checking mechanism. Instead of manually managing fibers with `fork` and `interrupt`, you can declare this relationship with `Effect.race`.\n\nThe key is that the polling effect is set up to repeat on a schedule that runs indefinitely (or for a very long time). Because it never completes on its own, it can never \"win\" the race. The main task is the only one that can complete successfully. When it does, it wins the race, and Effect's structured concurrency guarantees that the losing effect (the poller) is safely interrupted.\n\nThis creates a self-contained, declarative, and leak-free unit of work.\n\n---\n\n## Good Example\n\nThis program simulates a long-running data processing job. While it's running, a separate effect polls for its status every 2 seconds. When the main job finishes after 10 seconds, the polling automatically stops.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\n// The main task that takes a long time to complete\nconst longRunningJob = Effect.log(\"Data processing complete!\").pipe(\n Effect.delay(Duration.seconds(10))\n);\n\n// The polling task that checks the status\nconst pollStatus = Effect.log(\"Polling for job status: In Progress...\");\n\n// A schedule that repeats the polling task every 2 seconds, forever\nconst pollingSchedule = Schedule.fixed(Duration.seconds(2));\n\n// The complete polling effect that will run indefinitely until interrupted\nconst repeatingPoller = pollStatus.pipe(Effect.repeat(pollingSchedule));\n\n// Race the main job against the poller.\n// The longRunningJob will win after 10 seconds, interrupting the poller.\nconst program = Effect.race(longRunningJob, repeatingPoller);\n\nEffect.runPromise(program);\n/*\nOutput:\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nPolling for job status: In Progress...\nData processing complete!\n*/\n```\n\n---\n\n## Anti-Pattern\n\nManually managing the lifecycle of the polling fiber. This is more verbose, imperative, and error-prone. You have to remember to interrupt the polling fiber in all possible exit paths (success, failure, etc.), which `Effect.race` does for you automatically.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\nimport { longRunningJob, repeatingPoller } from \"./somewhere\";\n\n// ❌ WRONG: Manual fiber management is complex.\nconst program = Effect.gen(function* () {\n // Manually fork the poller into the background\n const pollerFiber = yield* Effect.fork(repeatingPoller);\n\n try {\n // Run the main job\n const result = yield* longRunningJob;\n return result;\n } finally {\n // You MUST remember to interrupt the poller when you're done.\n yield* Fiber.interrupt(pollerFiber);\n }\n});\n```" - }, - { - "id": "resource-pooling", - "title": "Pool Resources for Reuse", - "description": "Use Pool to manage expensive resources that can be reused across operations.", - "skillLevel": "intermediate", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Pool, Scope, Duration } from \"effect\"\n\n// ============================================\n// 1. Define a poolable resource\n// ============================================\n\ninterface DatabaseConnection {\n readonly id: number\n readonly query: (sql: string) => Effect.Effect\n readonly close: () => Effect.Effect\n}\n\nlet connectionId = 0\n\nconst createConnection = Effect.gen(function* () {\n const id = ++connectionId\n yield* Effect.log(`Creating connection ${id}`)\n \n // Simulate connection setup time\n yield* Effect.sleep(\"100 millis\")\n \n const connection: DatabaseConnection = {\n id,\n query: (sql) => Effect.gen(function* () {\n yield* Effect.log(`[Conn ${id}] Executing: ${sql}`)\n return [{ result: \"data\" }]\n }),\n close: () => Effect.gen(function* () {\n yield* Effect.log(`Closing connection ${id}`)\n }),\n }\n \n return connection\n})\n\n// ============================================\n// 2. Create a pool\n// ============================================\n\nconst makeConnectionPool = Pool.make({\n acquire: createConnection,\n size: 5, // Maximum 5 connections\n})\n\n// ============================================\n// 3. Use the pool\n// ============================================\n\nconst runQuery = (pool: Pool.Pool, sql: string) =>\n Effect.scoped(\n Effect.gen(function* () {\n // Get a connection from the pool\n const connection = yield* pool.get\n \n // Use it\n const results = yield* connection.query(sql)\n \n // Connection automatically returned to pool when scope ends\n return results\n })\n )\n\n// ============================================\n// 4. Run multiple queries concurrently\n// ============================================\n\nconst program = Effect.scoped(\n Effect.gen(function* () {\n const pool = yield* makeConnectionPool\n \n yield* Effect.log(\"Starting concurrent queries...\")\n \n // Run 10 queries with only 5 connections\n const queries = Array.from({ length: 10 }, (_, i) =>\n runQuery(pool, `SELECT * FROM users WHERE id = ${i}`)\n )\n \n const results = yield* Effect.all(queries, { concurrency: \"unbounded\" })\n \n yield* Effect.log(`Completed ${results.length} queries`)\n return results\n })\n)\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Creating resources is expensive:\n\n1. **Database connections** - TCP handshake, authentication\n2. **HTTP clients** - Connection setup, TLS negotiation\n3. **Worker threads** - Spawn overhead\n4. **File handles** - System calls\n\nPooling amortizes this cost across many operations.\n\n---", - "content": "## Guideline\n\nUse `Pool` to manage a collection of reusable resources. The pool handles acquisition, release, and lifecycle management automatically.\n\n---\n\n## Rationale\n\nCreating resources is expensive:\n\n1. **Database connections** - TCP handshake, authentication\n2. **HTTP clients** - Connection setup, TLS negotiation\n3. **Worker threads** - Spawn overhead\n4. **File handles** - System calls\n\nPooling amortizes this cost across many operations.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Pool, Scope, Duration } from \"effect\"\n\n// ============================================\n// 1. Define a poolable resource\n// ============================================\n\ninterface DatabaseConnection {\n readonly id: number\n readonly query: (sql: string) => Effect.Effect\n readonly close: () => Effect.Effect\n}\n\nlet connectionId = 0\n\nconst createConnection = Effect.gen(function* () {\n const id = ++connectionId\n yield* Effect.log(`Creating connection ${id}`)\n \n // Simulate connection setup time\n yield* Effect.sleep(\"100 millis\")\n \n const connection: DatabaseConnection = {\n id,\n query: (sql) => Effect.gen(function* () {\n yield* Effect.log(`[Conn ${id}] Executing: ${sql}`)\n return [{ result: \"data\" }]\n }),\n close: () => Effect.gen(function* () {\n yield* Effect.log(`Closing connection ${id}`)\n }),\n }\n \n return connection\n})\n\n// ============================================\n// 2. Create a pool\n// ============================================\n\nconst makeConnectionPool = Pool.make({\n acquire: createConnection,\n size: 5, // Maximum 5 connections\n})\n\n// ============================================\n// 3. Use the pool\n// ============================================\n\nconst runQuery = (pool: Pool.Pool, sql: string) =>\n Effect.scoped(\n Effect.gen(function* () {\n // Get a connection from the pool\n const connection = yield* pool.get\n \n // Use it\n const results = yield* connection.query(sql)\n \n // Connection automatically returned to pool when scope ends\n return results\n })\n )\n\n// ============================================\n// 4. Run multiple queries concurrently\n// ============================================\n\nconst program = Effect.scoped(\n Effect.gen(function* () {\n const pool = yield* makeConnectionPool\n \n yield* Effect.log(\"Starting concurrent queries...\")\n \n // Run 10 queries with only 5 connections\n const queries = Array.from({ length: 10 }, (_, i) =>\n runQuery(pool, `SELECT * FROM users WHERE id = ${i}`)\n )\n \n const results = yield* Effect.all(queries, { concurrency: \"unbounded\" })\n \n yield* Effect.log(`Completed ${results.length} queries`)\n return results\n })\n)\n\nEffect.runPromise(program)\n```\n\n## Pool Configuration\n\n| Option | Purpose |\n|--------|---------|\n| `size` | Maximum number of resources |\n| `acquire` | How to create a new resource |\n| `timeToLive` | Max time a resource lives |\n| `timeToLiveStrategy` | When to check TTL |\n\n## Pool with TTL\n\n```typescript\nconst poolWithTTL = Pool.make({\n acquire: createConnection,\n size: 10,\n timeToLive: Duration.minutes(5), // Refresh connections every 5 min\n})\n```\n\n## When to Use Pools\n\n| Resource | Pool? | Why |\n|----------|-------|-----|\n| DB connections | ✅ Yes | Expensive to create |\n| HTTP clients | ✅ Yes | Connection reuse |\n| Worker threads | ✅ Yes | Spawn overhead |\n| File handles | Maybe | Depends on usage |\n| Memory buffers | Maybe | If allocation is slow |" - }, - { - "id": "process-collection-in-parallel-with-foreach", - "title": "Process a Collection in Parallel with Effect.forEach", - "description": "Use Effect.forEach with the `concurrency` option to process a collection in parallel with a fixed limit.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "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.\n\n```typescript\nimport { Clock, Effect } from \"effect\";\n\n// Mock function to simulate fetching a user by ID\nconst fetchUserById = (id: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user ${id}...`);\n yield* Effect.sleep(\"1 second\"); // Simulate network delay\n return { id, name: `User ${id}`, email: `user${id}@example.com` };\n });\n\nconst userIds = Array.from({ length: 10 }, (_, i) => i + 1);\n\n// Process the entire array, but only run 5 fetches at a time.\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting parallel processing...\");\n\n const startTime = yield* Clock.currentTimeMillis;\n const users = yield* Effect.forEach(userIds, fetchUserById, {\n concurrency: 5, // Limit to 5 concurrent operations\n });\n const endTime = yield* Clock.currentTimeMillis;\n\n yield* Effect.logInfo(\n `Processed ${users.length} users in ${endTime - startTime}ms`\n );\n yield* Effect.logInfo(\n `First few users: ${JSON.stringify(users.slice(0, 3), null, 2)}`\n );\n\n return users;\n});\n\n// The result will be an array of all user objects.\n// The total time will be much less than running them sequentially.\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "The anti-pattern is using `Effect.all` to process a large or dynamically-sized collection. This can lead to unpredictable and potentially catastrophic resource consumption.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { userIds, fetchUserById } from \"./somewhere\"; // From previous example\n\n// ❌ DANGEROUS: This will attempt to start 100 concurrent network requests.\n// If userIds had 10,000 items, this could crash your application or get you blocked by an API.\nconst program = Effect.all(userIds.map(fetchUserById));\n```", - "explanation": "Running `Effect.all` on a large array of tasks is dangerous. If you have 1,000 items, it will try to start 1,000 concurrent fibers at once, which can exhaust memory, overwhelm your CPU, or hit API rate limits.\n\n`Effect.forEach` with a concurrency limit solves this problem elegantly. It acts as a concurrent processing pool. It will start processing items up to your specified limit (e.g., 10 at a time). As soon as one task finishes, it will pick up the next available item from the list, ensuring that no more than 10 tasks are ever running simultaneously. This provides massive performance gains over sequential processing while maintaining stability and control.\n\n---", - "content": "## Guideline\n\nTo process an iterable (like an array) of items concurrently, use `Effect.forEach`. To avoid overwhelming systems, always specify the `{ concurrency: number }` option to limit how many effects run at the same time.\n\n---\n\n## Rationale\n\nRunning `Effect.all` on a large array of tasks is dangerous. If you have 1,000 items, it will try to start 1,000 concurrent fibers at once, which can exhaust memory, overwhelm your CPU, or hit API rate limits.\n\n`Effect.forEach` with a concurrency limit solves this problem elegantly. It acts as a concurrent processing pool. It will start processing items up to your specified limit (e.g., 10 at a time). As soon as one task finishes, it will pick up the next available item from the list, ensuring that no more than 10 tasks are ever running simultaneously. This provides massive performance gains over sequential processing while maintaining stability and control.\n\n---\n\n## Good Example\n\nImagine 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.\n\n```typescript\nimport { Clock, Effect } from \"effect\";\n\n// Mock function to simulate fetching a user by ID\nconst fetchUserById = (id: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user ${id}...`);\n yield* Effect.sleep(\"1 second\"); // Simulate network delay\n return { id, name: `User ${id}`, email: `user${id}@example.com` };\n });\n\nconst userIds = Array.from({ length: 10 }, (_, i) => i + 1);\n\n// Process the entire array, but only run 5 fetches at a time.\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting parallel processing...\");\n\n const startTime = yield* Clock.currentTimeMillis;\n const users = yield* Effect.forEach(userIds, fetchUserById, {\n concurrency: 5, // Limit to 5 concurrent operations\n });\n const endTime = yield* Clock.currentTimeMillis;\n\n yield* Effect.logInfo(\n `Processed ${users.length} users in ${endTime - startTime}ms`\n );\n yield* Effect.logInfo(\n `First few users: ${JSON.stringify(users.slice(0, 3), null, 2)}`\n );\n\n return users;\n});\n\n// The result will be an array of all user objects.\n// The total time will be much less than running them sequentially.\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nThe anti-pattern is using `Effect.all` to process a large or dynamically-sized collection. This can lead to unpredictable and potentially catastrophic resource consumption.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { userIds, fetchUserById } from \"./somewhere\"; // From previous example\n\n// ❌ DANGEROUS: This will attempt to start 100 concurrent network requests.\n// If userIds had 10,000 items, this could crash your application or get you blocked by an API.\nconst program = Effect.all(userIds.map(fetchUserById));\n```" - }, - { - "id": "stream-from-file", - "title": "Process a Large File with Constant Memory", - "description": "Use Stream.fromReadable with a Node.js Readable stream to process files efficiently.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { FileSystem } from \"@effect/platform\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport type { PlatformError } from \"@effect/platform/Error\";\nimport { Effect, Stream } from \"effect\";\nimport * as path from \"node:path\";\n\nconst processFile = (\n filePath: string,\n content: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n\n // Write content to file\n yield* fs.writeFileString(filePath, content);\n\n // Create a STREAMING pipeline - reads file in chunks, not all at once\n const fileStream = fs.readFile(filePath).pipe(\n // Decode bytes to text\n Stream.decodeText(\"utf-8\"),\n // Split into lines\n Stream.splitLines,\n // Process each line\n Stream.tap((line) => Effect.log(`Processing: ${line}`))\n );\n\n // Run the stream to completion\n yield* Stream.runDrain(fileStream);\n\n // Clean up file\n yield* fs.remove(filePath);\n });\n\nconst program = Effect.gen(function* () {\n const filePath = path.join(__dirname, \"large-file.txt\");\n\n yield* processFile(filePath, \"line 1\\nline 2\\nline 3\").pipe(\n Effect.catchAll((error: PlatformError) =>\n Effect.logError(`Error processing file: ${error.message}`)\n )\n );\n});\n\nEffect.runPromise(program.pipe(Effect.provide(NodeFileSystem.layer)));\n\n/*\nOutput:\n... level=INFO msg=\"Processing: line 1\"\n... level=INFO msg=\"Processing: line 2\"\n... level=INFO msg=\"Processing: line 3\"\n*/\n```", - "antiPattern": "The anti-pattern is to use synchronous, memory-intensive functions like `fs.readFileSync`. This approach is simple for tiny files but fails catastrophically for large ones.\n\n```typescript\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nconst filePath = path.join(__dirname, \"large-file.txt\");\n// Create a dummy file for the example\nfs.writeFileSync(filePath, \"line 1\\nline 2\\nline 3\");\n\ntry {\n // Anti-pattern: This loads the ENTIRE file into memory as a single buffer.\n const fileContent = fs.readFileSync(filePath, \"utf-8\");\n const lines = fileContent.split(\"\\n\");\n\n for (const line of lines) {\n console.log(`Processing: ${line}`);\n }\n} catch (err) {\n console.error(\"Failed to read file:\", err);\n} finally {\n // Clean up the dummy file\n fs.unlinkSync(filePath);\n}\n```\n\nThis is a dangerous anti-pattern because:\n\n1. **It's a Memory Bomb**: If `large-file.txt` were 2GB and your server had 1GB of RAM, this code would immediately crash the process.\n2. **It Blocks the Event Loop**: `readFileSync` is a synchronous, blocking operation. While it's reading the file from disk, your entire application is frozen and cannot respond to any other requests.\n3. **It's Not Composable**: You get a giant string that must be processed eagerly. You lose all the benefits of lazy processing, concurrency control, and integrated error handling that `Stream` provides.", - "explanation": "The most significant advantage of a streaming architecture is its ability to handle datasets far larger than available RAM. When you need to process a multi-gigabyte log file or CSV, loading it all into memory is not an option—it will crash your application.\n\nThe `Stream.fromReadable` constructor provides a bridge from Node.js's built-in file streaming capabilities to the Effect ecosystem. This approach is superior because:\n\n1. **Constant Memory Usage**: The file is read in small, manageable chunks. Your application's memory usage remains low and constant, regardless of whether the file is 1 megabyte or 100 gigabytes.\n2. **Composability**: Once the file is represented as an Effect `Stream`, you can apply the full suite of powerful operators to it: `mapEffect` for concurrent processing, `filter` for selectively choosing lines, `grouped` for batching, and `retry` for resilience.\n3. **Resource Safety**: Effect's `Stream` is built on `Scope`, which guarantees that the underlying file handle will be closed automatically when the stream finishes, fails, or is interrupted. This prevents resource leaks, a common problem in manual file handling.\n\n---", - "content": "## Guideline\n\nTo process a large file without consuming excessive memory, create a Node.js `Readable` stream from the file and convert it into an Effect `Stream` using `Stream.fromReadable`.\n\n---\n\n## Rationale\n\nThe most significant advantage of a streaming architecture is its ability to handle datasets far larger than available RAM. When you need to process a multi-gigabyte log file or CSV, loading it all into memory is not an option—it will crash your application.\n\nThe `Stream.fromReadable` constructor provides a bridge from Node.js's built-in file streaming capabilities to the Effect ecosystem. This approach is superior because:\n\n1. **Constant Memory Usage**: The file is read in small, manageable chunks. Your application's memory usage remains low and constant, regardless of whether the file is 1 megabyte or 100 gigabytes.\n2. **Composability**: Once the file is represented as an Effect `Stream`, you can apply the full suite of powerful operators to it: `mapEffect` for concurrent processing, `filter` for selectively choosing lines, `grouped` for batching, and `retry` for resilience.\n3. **Resource Safety**: Effect's `Stream` is built on `Scope`, which guarantees that the underlying file handle will be closed automatically when the stream finishes, fails, or is interrupted. This prevents resource leaks, a common problem in manual file handling.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { FileSystem } from \"@effect/platform\";\nimport { NodeFileSystem } from \"@effect/platform-node\";\nimport type { PlatformError } from \"@effect/platform/Error\";\nimport { Effect, Stream } from \"effect\";\nimport * as path from \"node:path\";\n\nconst processFile = (\n filePath: string,\n content: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n\n // Write content to file\n yield* fs.writeFileString(filePath, content);\n\n // Create a STREAMING pipeline - reads file in chunks, not all at once\n const fileStream = fs.readFile(filePath).pipe(\n // Decode bytes to text\n Stream.decodeText(\"utf-8\"),\n // Split into lines\n Stream.splitLines,\n // Process each line\n Stream.tap((line) => Effect.log(`Processing: ${line}`))\n );\n\n // Run the stream to completion\n yield* Stream.runDrain(fileStream);\n\n // Clean up file\n yield* fs.remove(filePath);\n });\n\nconst program = Effect.gen(function* () {\n const filePath = path.join(__dirname, \"large-file.txt\");\n\n yield* processFile(filePath, \"line 1\\nline 2\\nline 3\").pipe(\n Effect.catchAll((error: PlatformError) =>\n Effect.logError(`Error processing file: ${error.message}`)\n )\n );\n});\n\nEffect.runPromise(program.pipe(Effect.provide(NodeFileSystem.layer)));\n\n/*\nOutput:\n... level=INFO msg=\"Processing: line 1\"\n... level=INFO msg=\"Processing: line 2\"\n... level=INFO msg=\"Processing: line 3\"\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to use synchronous, memory-intensive functions like `fs.readFileSync`. This approach is simple for tiny files but fails catastrophically for large ones.\n\n```typescript\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nconst filePath = path.join(__dirname, \"large-file.txt\");\n// Create a dummy file for the example\nfs.writeFileSync(filePath, \"line 1\\nline 2\\nline 3\");\n\ntry {\n // Anti-pattern: This loads the ENTIRE file into memory as a single buffer.\n const fileContent = fs.readFileSync(filePath, \"utf-8\");\n const lines = fileContent.split(\"\\n\");\n\n for (const line of lines) {\n console.log(`Processing: ${line}`);\n }\n} catch (err) {\n console.error(\"Failed to read file:\", err);\n} finally {\n // Clean up the dummy file\n fs.unlinkSync(filePath);\n}\n```\n\nThis is a dangerous anti-pattern because:\n\n1. **It's a Memory Bomb**: If `large-file.txt` were 2GB and your server had 1GB of RAM, this code would immediately crash the process.\n2. **It Blocks the Event Loop**: `readFileSync` is a synchronous, blocking operation. While it's reading the file from disk, your entire application is frozen and cannot respond to any other requests.\n3. **It's Not Composable**: You get a giant string that must be processed eagerly. You lose all the benefits of lazy processing, concurrency control, and integrated error handling that `Stream` provides." - }, - { - "id": "process-a-collection-of-data-asynchronously", - "title": "Process collections of data asynchronously", - "description": "Leverage Stream to process collections effectfully with built-in concurrency control and resource safety.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A mock function that simulates fetching a user from a database\nconst getUserById = (\n id: number\n): Effect.Effect<{ id: number; name: string }, Error> =>\n Effect.succeed({ id, name: `User ${id}` }).pipe(\n Effect.delay(\"100 millis\"),\n Effect.tap(() => Effect.log(`Fetched user ${id}`))\n );\n\n// The stream-based program\nconst program = Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n // Process each item with an Effect, limiting concurrency to 2\n Stream.mapEffect(getUserById, { concurrency: 2 }),\n // Run the stream and collect all results into a Chunk\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const users = yield* program;\n yield* Effect.log(\n `All users fetched: ${JSON.stringify(Chunk.toArray(users))}`\n );\n return users;\n});\n\nEffect.runPromise(programWithLogging);\n```", - "antiPattern": "A common but flawed approach is to use `Promise.all` to handle multiple asynchronous operations. This method lacks the safety, control, and composability inherent to Effect's `Stream`.\n\n```typescript\n// A mock function that returns a Promise\nconst getUserByIdAsPromise = (\n id: number\n): Promise<{ id: number; name: string }> =>\n new Promise((resolve) => {\n setTimeout(() => {\n console.log(`Fetched user ${id}`);\n resolve({ id, name: `User ${id}` });\n }, 100);\n });\n\n// The Promise-based program\nconst ids = [1, 2, 3, 4, 5];\nconst promises = ids.map(getUserByIdAsPromise);\n\nPromise.all(promises).then((users) => {\n console.log(\"All users fetched:\", users);\n});\n```\n\nThis anti-pattern is problematic because it immediately executes all promises in parallel with no concurrency limit, it does not benefit from Effect's structured concurrency for safe interruption, and it breaks out of the Effect context, losing composability with features like logging, retries, and dependency management.", - "explanation": "`Stream` is a fundamental data type in Effect for handling collections of data, especially in asynchronous contexts. Unlike a simple array, a `Stream` is lazy and pull-based, meaning it only computes or fetches elements as they are needed, making it highly efficient for large or infinite datasets.\n\nThe primary benefits of using `Stream` are:\n\n1. **Concurrency Control**: `Stream` provides powerful and simple operators like `mapEffect` that have built-in concurrency management. This prevents overwhelming downstream services with too many parallel requests.\n2. **Resource Safety**: `Stream` is built on `Scope`, ensuring that any resources opened during the stream's operation (like file handles or network connections) are safely and reliably closed, even in the case of errors or interruption.\n3. **Composability**: Streams are highly composable. They can be filtered, mapped, transformed, and combined with other Effect data types seamlessly, allowing you to build complex data processing pipelines that remain readable and type-safe.\n4. **Resilience**: `Stream` integrates with `Schedule` to provide sophisticated retry and repeat logic, and with Effect's structured concurrency to ensure that failures in one part of a pipeline lead to a clean and predictable shutdown of the entire process.\n\n---", - "content": "## Guideline\n\nFor processing collections that involve asynchronous or effectful operations, use `Stream` to ensure resource safety, control concurrency, and maintain composability.\n\n---\n\n## Rationale\n\n`Stream` is a fundamental data type in Effect for handling collections of data, especially in asynchronous contexts. Unlike a simple array, a `Stream` is lazy and pull-based, meaning it only computes or fetches elements as they are needed, making it highly efficient for large or infinite datasets.\n\nThe primary benefits of using `Stream` are:\n\n1. **Concurrency Control**: `Stream` provides powerful and simple operators like `mapEffect` that have built-in concurrency management. This prevents overwhelming downstream services with too many parallel requests.\n2. **Resource Safety**: `Stream` is built on `Scope`, ensuring that any resources opened during the stream's operation (like file handles or network connections) are safely and reliably closed, even in the case of errors or interruption.\n3. **Composability**: Streams are highly composable. They can be filtered, mapped, transformed, and combined with other Effect data types seamlessly, allowing you to build complex data processing pipelines that remain readable and type-safe.\n4. **Resilience**: `Stream` integrates with `Schedule` to provide sophisticated retry and repeat logic, and with Effect's structured concurrency to ensure that failures in one part of a pipeline lead to a clean and predictable shutdown of the entire process.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A mock function that simulates fetching a user from a database\nconst getUserById = (\n id: number\n): Effect.Effect<{ id: number; name: string }, Error> =>\n Effect.succeed({ id, name: `User ${id}` }).pipe(\n Effect.delay(\"100 millis\"),\n Effect.tap(() => Effect.log(`Fetched user ${id}`))\n );\n\n// The stream-based program\nconst program = Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n // Process each item with an Effect, limiting concurrency to 2\n Stream.mapEffect(getUserById, { concurrency: 2 }),\n // Run the stream and collect all results into a Chunk\n Stream.runCollect\n);\n\nconst programWithLogging = Effect.gen(function* () {\n const users = yield* program;\n yield* Effect.log(\n `All users fetched: ${JSON.stringify(Chunk.toArray(users))}`\n );\n return users;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n## Anti-Pattern\n\nA common but flawed approach is to use `Promise.all` to handle multiple asynchronous operations. This method lacks the safety, control, and composability inherent to Effect's `Stream`.\n\n```typescript\n// A mock function that returns a Promise\nconst getUserByIdAsPromise = (\n id: number\n): Promise<{ id: number; name: string }> =>\n new Promise((resolve) => {\n setTimeout(() => {\n console.log(`Fetched user ${id}`);\n resolve({ id, name: `User ${id}` });\n }, 100);\n });\n\n// The Promise-based program\nconst ids = [1, 2, 3, 4, 5];\nconst promises = ids.map(getUserByIdAsPromise);\n\nPromise.all(promises).then((users) => {\n console.log(\"All users fetched:\", users);\n});\n```\n\nThis anti-pattern is problematic because it immediately executes all promises in parallel with no concurrency limit, it does not benefit from Effect's structured concurrency for safe interruption, and it breaks out of the Effect context, losing composability with features like logging, retries, and dependency management." - }, - { - "id": "stream-process-concurrently", - "title": "Process Items Concurrently", - "description": "Use Stream.mapEffect with the `concurrency` option to process stream items in parallel.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// A mock function that simulates a slow I/O operation\nconst processItem = (id: number): Effect.Effect =>\n Effect.log(`Starting item ${id}...`).pipe(\n Effect.delay(\"1 second\"),\n Effect.map(() => `Finished item ${id}`),\n Effect.tap(Effect.log)\n );\n\nconst ids = [1, 2, 3, 4];\n\nconst program = Stream.fromIterable(ids).pipe(\n // Process up to 2 items concurrently\n Stream.mapEffect(processItem, { concurrency: 2 }),\n Stream.runDrain\n);\n\n// Measure the total time taken\nconst timedProgram = Effect.timed(program);\n\nconst programWithLogging = Effect.gen(function* () {\n const [duration, _] = yield* timedProgram;\n const durationMs = Number(duration);\n yield* Effect.log(`\\nTotal time: ${Math.round(durationMs / 1000)} seconds`);\n return duration;\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n... level=INFO msg=\"Starting item 1...\"\n... level=INFO msg=\"Starting item 2...\"\n... level=INFO msg=\"Finished item 1\"\n... level=INFO msg=\"Starting item 3...\"\n... level=INFO msg=\"Finished item 2\"\n... level=INFO msg=\"Starting item 4...\"\n... level=INFO msg=\"Finished item 3\"\n... level=INFO msg=\"Finished item 4\"\n\nTotal time: 2 seconds\n*/\n```", - "antiPattern": "The anti-pattern is to process I/O-bound tasks sequentially. This is the default behavior of `Stream.mapEffect` if you don't specify a concurrency level, and it leads to poor performance.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same processItem function ...\n\nconst ids = [1, 2, 3, 4];\n\n// Processing sequentially (default concurrency is 1)\nconst program = Stream.fromIterable(ids).pipe(\n Stream.mapEffect(processItem), // No concurrency option\n Stream.runDrain\n);\n\nconst timedProgram = Effect.timed(program);\n\nEffect.runPromise(timedProgram).then(([duration, _]) => {\n console.log(`\\nTotal time: ${Math.round(duration.millis / 1000)} seconds`);\n});\n/*\nOutput:\n... level=INFO msg=\"Starting item 1...\"\n... level=INFO msg=\"Finished item 1\"\n... level=INFO msg=\"Starting item 2...\"\n... level=INFO msg=\"Finished item 2\"\n... etc.\n\nTotal time: 4 seconds\n*/\n```\n\nWhile sequential processing is sometimes necessary to preserve order or avoid race conditions, it is a performance anti-pattern for independent, I/O-bound tasks. The concurrent approach is almost always preferable in such cases.", - "explanation": "For many data pipelines, the most time-consuming step is performing an I/O-bound operation for each item, such as calling an API or querying a database. Processing these items one by one (sequentially) is safe but slow, as the entire pipeline waits for each operation to complete before starting the next.\n\n`Stream.mapEffect`'s `concurrency` option is the solution. It provides a simple, declarative way to introduce controlled parallelism into your pipeline.\n\n1. **Performance Boost**: It allows the stream to work on multiple items at once, drastically reducing the total execution time for I/O-bound tasks.\n2. **Controlled Parallelism**: Unlike `Promise.all` which runs everything at once, you specify the _exact_ number of concurrent operations. This is crucial for stability, as it prevents your application from overwhelming downstream services or exhausting its own resources (like file handles or network sockets).\n3. **Automatic Backpressure**: The stream will not pull new items from the source faster than the concurrent slots can process them. This backpressure is handled automatically, preventing memory issues.\n4. **Structured Concurrency**: It's fully integrated with Effect's runtime. If any concurrent operation fails, all other in-flight operations for that stream are immediately and reliably interrupted, preventing wasted work and ensuring clean shutdowns.\n\n---", - "content": "## Guideline\n\nTo process items in a stream concurrently, use `Stream.mapEffect` and provide a value greater than 1 to its `concurrency` option.\n\n---\n\n## Rationale\n\nFor many data pipelines, the most time-consuming step is performing an I/O-bound operation for each item, such as calling an API or querying a database. Processing these items one by one (sequentially) is safe but slow, as the entire pipeline waits for each operation to complete before starting the next.\n\n`Stream.mapEffect`'s `concurrency` option is the solution. It provides a simple, declarative way to introduce controlled parallelism into your pipeline.\n\n1. **Performance Boost**: It allows the stream to work on multiple items at once, drastically reducing the total execution time for I/O-bound tasks.\n2. **Controlled Parallelism**: Unlike `Promise.all` which runs everything at once, you specify the _exact_ number of concurrent operations. This is crucial for stability, as it prevents your application from overwhelming downstream services or exhausting its own resources (like file handles or network sockets).\n3. **Automatic Backpressure**: The stream will not pull new items from the source faster than the concurrent slots can process them. This backpressure is handled automatically, preventing memory issues.\n4. **Structured Concurrency**: It's fully integrated with Effect's runtime. If any concurrent operation fails, all other in-flight operations for that stream are immediately and reliably interrupted, preventing wasted work and ensuring clean shutdowns.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// A mock function that simulates a slow I/O operation\nconst processItem = (id: number): Effect.Effect =>\n Effect.log(`Starting item ${id}...`).pipe(\n Effect.delay(\"1 second\"),\n Effect.map(() => `Finished item ${id}`),\n Effect.tap(Effect.log)\n );\n\nconst ids = [1, 2, 3, 4];\n\nconst program = Stream.fromIterable(ids).pipe(\n // Process up to 2 items concurrently\n Stream.mapEffect(processItem, { concurrency: 2 }),\n Stream.runDrain\n);\n\n// Measure the total time taken\nconst timedProgram = Effect.timed(program);\n\nconst programWithLogging = Effect.gen(function* () {\n const [duration, _] = yield* timedProgram;\n const durationMs = Number(duration);\n yield* Effect.log(`\\nTotal time: ${Math.round(durationMs / 1000)} seconds`);\n return duration;\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n... level=INFO msg=\"Starting item 1...\"\n... level=INFO msg=\"Starting item 2...\"\n... level=INFO msg=\"Finished item 1\"\n... level=INFO msg=\"Starting item 3...\"\n... level=INFO msg=\"Finished item 2\"\n... level=INFO msg=\"Starting item 4...\"\n... level=INFO msg=\"Finished item 3\"\n... level=INFO msg=\"Finished item 4\"\n\nTotal time: 2 seconds\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to process I/O-bound tasks sequentially. This is the default behavior of `Stream.mapEffect` if you don't specify a concurrency level, and it leads to poor performance.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same processItem function ...\n\nconst ids = [1, 2, 3, 4];\n\n// Processing sequentially (default concurrency is 1)\nconst program = Stream.fromIterable(ids).pipe(\n Stream.mapEffect(processItem), // No concurrency option\n Stream.runDrain\n);\n\nconst timedProgram = Effect.timed(program);\n\nEffect.runPromise(timedProgram).then(([duration, _]) => {\n console.log(`\\nTotal time: ${Math.round(duration.millis / 1000)} seconds`);\n});\n/*\nOutput:\n... level=INFO msg=\"Starting item 1...\"\n... level=INFO msg=\"Finished item 1\"\n... level=INFO msg=\"Starting item 2...\"\n... level=INFO msg=\"Finished item 2\"\n... etc.\n\nTotal time: 4 seconds\n*/\n```\n\nWhile sequential processing is sometimes necessary to preserve order or avoid race conditions, it is a performance anti-pattern for independent, I/O-bound tasks. The concurrent approach is almost always preferable in such cases." - }, - { - "id": "stream-process-in-batches", - "title": "Process Items in Batches", - "description": "Use Stream.grouped(n) to transform a stream of items into a stream of batched chunks.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A mock function that simulates a bulk database insert\nconst saveUsersInBulk = (\n userBatch: Chunk.Chunk<{ id: number }>\n): Effect.Effect =>\n Effect.log(\n `Saving batch of ${userBatch.length} users: ${Chunk.toArray(userBatch)\n .map((u) => u.id)\n .join(\", \")}`\n );\n\nconst userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));\n\nconst program = Stream.fromIterable(userIds).pipe(\n // Group the stream of users into batches of 5\n Stream.grouped(5),\n // Process each batch with our bulk save function\n Stream.mapEffect(saveUsersInBulk, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program);\n/*\nOutput:\n... level=INFO msg=\"Saving batch of 5 users: 1, 2, 3, 4, 5\"\n... level=INFO msg=\"Saving batch of 5 users: 6, 7, 8, 9, 10\"\n*/\n```", - "antiPattern": "The anti-pattern is to process items one by one when a more efficient bulk operation is available. This is a common performance bottleneck.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// A mock function that saves one user at a time\nconst saveUser = (user: { id: number }): Effect.Effect =>\n Effect.log(`Saving single user: ${user.id}`);\n\nconst userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));\n\nconst program = Stream.fromIterable(userIds).pipe(\n // Process each user individually, leading to 10 separate \"saves\"\n Stream.mapEffect(saveUser, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program);\n/*\nOutput:\n... level=INFO msg=\"Saving single user: 1\"\n... level=INFO msg=\"Saving single user: 2\"\n... (and so on for all 10 users)\n*/\n```\n\nThis individual processing approach is an anti-pattern because it creates unnecessary overhead. If each `saveUser` call took 50ms of network latency, the total time would be over 500ms. The batched approach might only take 100ms (2 batches \\* 50ms), resulting in a 5x performance improvement.", - "explanation": "When interacting with external systems like databases or APIs, making one request per item is often incredibly inefficient. The network latency and overhead of each individual call can dominate the total processing time. Most high-performance systems offer bulk or batch endpoints to mitigate this.\n\n`Stream.grouped(n)` provides a simple, declarative way to prepare your data for these bulk operations:\n\n1. **Performance Optimization**: It dramatically reduces the number of network roundtrips. A single API call with 100 items is far faster than 100 individual API calls.\n2. **Declarative Batching**: It abstracts away the tedious and error-prone manual logic of counting items, managing temporary buffers, and deciding when to send a batch.\n3. **Seamless Composition**: It transforms a `Stream` into a `Stream>`. This new stream of chunks can be piped directly into `Stream.mapEffect`, allowing you to process each batch concurrently.\n4. **Handles Leftovers**: The operator automatically handles the final, smaller batch if the total number of items is not perfectly divisible by the batch size.\n\n---", - "content": "## Guideline\n\nTo process items in fixed-size batches for performance, use the `Stream.grouped(batchSize)` operator to transform a stream of individual items into a stream of `Chunk`s.\n\n---\n\n## Rationale\n\nWhen interacting with external systems like databases or APIs, making one request per item is often incredibly inefficient. The network latency and overhead of each individual call can dominate the total processing time. Most high-performance systems offer bulk or batch endpoints to mitigate this.\n\n`Stream.grouped(n)` provides a simple, declarative way to prepare your data for these bulk operations:\n\n1. **Performance Optimization**: It dramatically reduces the number of network roundtrips. A single API call with 100 items is far faster than 100 individual API calls.\n2. **Declarative Batching**: It abstracts away the tedious and error-prone manual logic of counting items, managing temporary buffers, and deciding when to send a batch.\n3. **Seamless Composition**: It transforms a `Stream` into a `Stream>`. This new stream of chunks can be piped directly into `Stream.mapEffect`, allowing you to process each batch concurrently.\n4. **Handles Leftovers**: The operator automatically handles the final, smaller batch if the total number of items is not perfectly divisible by the batch size.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A mock function that simulates a bulk database insert\nconst saveUsersInBulk = (\n userBatch: Chunk.Chunk<{ id: number }>\n): Effect.Effect =>\n Effect.log(\n `Saving batch of ${userBatch.length} users: ${Chunk.toArray(userBatch)\n .map((u) => u.id)\n .join(\", \")}`\n );\n\nconst userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));\n\nconst program = Stream.fromIterable(userIds).pipe(\n // Group the stream of users into batches of 5\n Stream.grouped(5),\n // Process each batch with our bulk save function\n Stream.mapEffect(saveUsersInBulk, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program);\n/*\nOutput:\n... level=INFO msg=\"Saving batch of 5 users: 1, 2, 3, 4, 5\"\n... level=INFO msg=\"Saving batch of 5 users: 6, 7, 8, 9, 10\"\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to process items one by one when a more efficient bulk operation is available. This is a common performance bottleneck.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\n// A mock function that saves one user at a time\nconst saveUser = (user: { id: number }): Effect.Effect =>\n Effect.log(`Saving single user: ${user.id}`);\n\nconst userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 }));\n\nconst program = Stream.fromIterable(userIds).pipe(\n // Process each user individually, leading to 10 separate \"saves\"\n Stream.mapEffect(saveUser, { concurrency: 1 }),\n Stream.runDrain\n);\n\nEffect.runPromise(program);\n/*\nOutput:\n... level=INFO msg=\"Saving single user: 1\"\n... level=INFO msg=\"Saving single user: 2\"\n... (and so on for all 10 users)\n*/\n```\n\nThis individual processing approach is an anti-pattern because it creates unnecessary overhead. If each `saveUser` call took 50ms of network latency, the total time would be over 500ms. The batched approach might only take 100ms (2 batches \\* 50ms), resulting in a 5x performance improvement." - }, - { - "id": "process-streaming-data-with-stream", - "title": "Process Streaming Data with Stream", - "description": "Use Stream to model and process data that arrives over time in a composable, efficient way.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "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.\n\n```typescript\nimport { Effect, Stream, Option } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\ninterface PaginatedResponse {\n users: User[];\n nextPage: number | null;\n}\n\n// A mock API call that returns a page of users\nconst fetchUserPage = (\n page: number\n): Effect.Effect =>\n Effect.succeed(\n page < 3\n ? {\n users: [\n { id: page * 2 + 1, name: `User ${page * 2 + 1}` },\n { id: page * 2 + 2, name: `User ${page * 2 + 2}` },\n ],\n nextPage: page + 1,\n }\n : { users: [], nextPage: null }\n ).pipe(Effect.delay(\"50 millis\"));\n\n// Stream.paginateEffect creates a stream from a paginated source\nconst userStream: Stream.Stream = Stream.paginateEffect(\n 0,\n (page) =>\n fetchUserPage(page).pipe(\n Effect.map(\n (response) =>\n [response.users, Option.fromNullable(response.nextPage)] as const\n )\n )\n).pipe(\n // Flatten the stream of user arrays into a stream of individual users\n Stream.flatMap((users) => Stream.fromIterable(users))\n);\n\n// We can now process the stream of users.\n// Stream.runForEach will pull from the stream until it's exhausted.\nconst program = Stream.runForEach(userStream, (user: User) =>\n Effect.log(`Processing user: ${user.name}`)\n);\n\nconst programWithErrorHandling = program.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Stream processing error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n---", - "antiPattern": "Manually managing pagination state with recursive functions. This is complex, stateful, and easy to get wrong. It also requires loading all results into memory, which is inefficient for large datasets.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { fetchUserPage } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: Manual, stateful, and inefficient recursion.\nconst fetchAllUsers = (\n page: number,\n acc: any[]\n): Effect.Effect =>\n fetchUserPage(page).pipe(\n Effect.flatMap((response) => {\n const allUsers = [...acc, ...response.users];\n if (response.nextPage) {\n return fetchAllUsers(response.nextPage, allUsers);\n }\n return Effect.succeed(allUsers);\n })\n );\n\n// This holds all users in memory at once.\nconst program = fetchAllUsers(0, []);\n```", - "explanation": "Some data sources don't fit the one-shot request/response model of `Effect`. For example:\n\n- Reading a multi-gigabyte file from disk.\n- Receiving messages from a WebSocket.\n- Fetching results from a paginated API.\n\nLoading all this data into memory at once would be inefficient or impossible. `Stream` solves this by allowing you to process the data in chunks as it arrives. It provides a rich API of composable operators (`map`, `filter`, `run`, etc.) that mirror those on `Effect` and `Array`, but are designed for streaming data. This allows you to build efficient, constant-memory data processing pipelines.\n\n---", - "content": "## Guideline\n\nWhen dealing with a sequence of data that arrives asynchronously, model it as a `Stream`. A `Stream` is like an asynchronous, effectful `Array`. It represents a sequence of values of type `A` that may fail with an error `E` and requires services `R`.\n\n---\n\n## Rationale\n\nSome data sources don't fit the one-shot request/response model of `Effect`. For example:\n\n- Reading a multi-gigabyte file from disk.\n- Receiving messages from a WebSocket.\n- Fetching results from a paginated API.\n\nLoading all this data into memory at once would be inefficient or impossible. `Stream` solves this by allowing you to process the data in chunks as it arrives. It provides a rich API of composable operators (`map`, `filter`, `run`, etc.) that mirror those on `Effect` and `Array`, but are designed for streaming data. This allows you to build efficient, constant-memory data processing pipelines.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream, Option } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n}\ninterface PaginatedResponse {\n users: User[];\n nextPage: number | null;\n}\n\n// A mock API call that returns a page of users\nconst fetchUserPage = (\n page: number\n): Effect.Effect =>\n Effect.succeed(\n page < 3\n ? {\n users: [\n { id: page * 2 + 1, name: `User ${page * 2 + 1}` },\n { id: page * 2 + 2, name: `User ${page * 2 + 2}` },\n ],\n nextPage: page + 1,\n }\n : { users: [], nextPage: null }\n ).pipe(Effect.delay(\"50 millis\"));\n\n// Stream.paginateEffect creates a stream from a paginated source\nconst userStream: Stream.Stream = Stream.paginateEffect(\n 0,\n (page) =>\n fetchUserPage(page).pipe(\n Effect.map(\n (response) =>\n [response.users, Option.fromNullable(response.nextPage)] as const\n )\n )\n).pipe(\n // Flatten the stream of user arrays into a stream of individual users\n Stream.flatMap((users) => Stream.fromIterable(users))\n);\n\n// We can now process the stream of users.\n// Stream.runForEach will pull from the stream until it's exhausted.\nconst program = Stream.runForEach(userStream, (user: User) =>\n Effect.log(`Processing user: ${user.name}`)\n);\n\nconst programWithErrorHandling = program.pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Stream processing error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n---\n\n## Anti-Pattern\n\nManually managing pagination state with recursive functions. This is complex, stateful, and easy to get wrong. It also requires loading all results into memory, which is inefficient for large datasets.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { fetchUserPage } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: Manual, stateful, and inefficient recursion.\nconst fetchAllUsers = (\n page: number,\n acc: any[]\n): Effect.Effect =>\n fetchUserPage(page).pipe(\n Effect.flatMap((response) => {\n const allUsers = [...acc, ...response.users];\n if (response.nextPage) {\n return fetchAllUsers(response.nextPage, allUsers);\n }\n return Effect.succeed(allUsers);\n })\n );\n\n// This holds all users in memory at once.\nconst program = fetchAllUsers(0, []);\n```" - }, - { - "id": "tooling-profiling", - "title": "Profile Effect Applications", - "description": "Use Effect's timing features and Node.js profilers to find performance bottlenecks.", - "skillLevel": "advanced", - "useCases": [ - "tooling-and-debugging" - ], - "example": "### 1. Basic Timing with Spans\n\n```typescript\nimport { Effect, Duration } from \"effect\"\n\n// ============================================\n// 1. Time individual operations\n// ============================================\n\nconst timeOperation = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n const result = yield* effect\n\n const duration = Date.now() - startTime\n yield* Effect.log(`${name}: ${duration}ms`)\n\n return result\n })\n\n// Usage\nconst program = Effect.gen(function* () {\n yield* timeOperation(\"database-query\", queryDatabase())\n yield* timeOperation(\"api-call\", callExternalApi())\n yield* timeOperation(\"processing\", processData())\n})\n\n// ============================================\n// 2. Use withLogSpan for nested timing\n// ============================================\n\nconst timedProgram = Effect.gen(function* () {\n yield* Effect.log(\"Starting\")\n\n yield* fetchUsers().pipe(Effect.withLogSpan(\"fetchUsers\"))\n\n yield* processUsers().pipe(Effect.withLogSpan(\"processUsers\"))\n\n yield* saveResults().pipe(Effect.withLogSpan(\"saveResults\"))\n\n yield* Effect.log(\"Complete\")\n}).pipe(Effect.withLogSpan(\"total\"))\n\n// ============================================\n// 3. Collect timing metrics\n// ============================================\n\nimport { Metric } from \"effect\"\n\nconst operationDuration = Metric.histogram(\"operation_duration_ms\", {\n description: \"Operation duration in milliseconds\",\n boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000],\n})\n\nconst profiledEffect = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n const result = yield* effect\n\n const duration = Date.now() - startTime\n yield* Metric.update(\n operationDuration.pipe(Metric.tagged(\"operation\", name)),\n duration\n )\n\n return result\n })\n\n// ============================================\n// 4. Memory profiling\n// ============================================\n\nconst logMemoryUsage = Effect.sync(() => {\n const usage = process.memoryUsage()\n return {\n heapUsed: Math.round(usage.heapUsed / 1024 / 1024),\n heapTotal: Math.round(usage.heapTotal / 1024 / 1024),\n external: Math.round(usage.external / 1024 / 1024),\n rss: Math.round(usage.rss / 1024 / 1024),\n }\n})\n\nconst withMemoryLogging = (effect: Effect.Effect) =>\n Effect.gen(function* () {\n const before = yield* logMemoryUsage\n yield* Effect.log(`Memory before: ${JSON.stringify(before)}MB`)\n\n const result = yield* effect\n\n const after = yield* logMemoryUsage\n yield* Effect.log(`Memory after: ${JSON.stringify(after)}MB`)\n yield* Effect.log(`Memory delta: ${after.heapUsed - before.heapUsed}MB`)\n\n return result\n })\n\n// ============================================\n// 5. CPU profiling with Node.js inspector\n// ============================================\n\nconst withCpuProfile = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n // Start CPU profiler (requires --inspect flag)\n const inspector = yield* Effect.try(() => {\n const { Session } = require(\"inspector\")\n const session = new Session()\n session.connect()\n return session\n })\n\n yield* Effect.try(() => {\n inspector.post(\"Profiler.enable\")\n inspector.post(\"Profiler.start\")\n })\n\n const result = yield* effect\n\n // Stop and save profile\n yield* Effect.async((resume) => {\n inspector.post(\"Profiler.stop\", (err: Error, { profile }: any) => {\n if (err) {\n resume(Effect.fail(err))\n } else {\n const fs = require(\"fs\")\n fs.writeFileSync(\n `${name}-${Date.now()}.cpuprofile`,\n JSON.stringify(profile)\n )\n resume(Effect.void)\n }\n })\n })\n\n return result\n })\n\n// ============================================\n// 6. Benchmark specific operations\n// ============================================\n\nconst benchmark = (\n name: string,\n effect: Effect.Effect,\n iterations: number = 100\n) =>\n Effect.gen(function* () {\n const times: number[] = []\n\n for (let i = 0; i < iterations; i++) {\n const start = performance.now()\n yield* effect\n times.push(performance.now() - start)\n }\n\n const sorted = times.sort((a, b) => a - b)\n const stats = {\n min: sorted[0],\n max: sorted[sorted.length - 1],\n median: sorted[Math.floor(sorted.length / 2)],\n p95: sorted[Math.floor(sorted.length * 0.95)],\n p99: sorted[Math.floor(sorted.length * 0.99)],\n mean: times.reduce((a, b) => a + b, 0) / times.length,\n }\n\n yield* Effect.log(`Benchmark \"${name}\" (${iterations} iterations):`)\n yield* Effect.log(` Min: ${stats.min.toFixed(2)}ms`)\n yield* Effect.log(` Max: ${stats.max.toFixed(2)}ms`)\n yield* Effect.log(` Mean: ${stats.mean.toFixed(2)}ms`)\n yield* Effect.log(` Median: ${stats.median.toFixed(2)}ms`)\n yield* Effect.log(` P95: ${stats.p95.toFixed(2)}ms`)\n yield* Effect.log(` P99: ${stats.p99.toFixed(2)}ms`)\n\n return stats\n })\n\n// ============================================\n// 7. Profile concurrent operations\n// ============================================\n\nconst profileConcurrency = Effect.gen(function* () {\n const items = Array.from({ length: 100 }, (_, i) => i)\n\n // Sequential\n yield* benchmark(\n \"sequential\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 1 }),\n 10\n )\n\n // Parallel unbounded\n yield* benchmark(\n \"parallel-unbounded\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), {\n concurrency: \"unbounded\",\n }),\n 10\n )\n\n // Parallel limited\n yield* benchmark(\n \"parallel-10\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 10 }),\n 10\n )\n})\n\n// ============================================\n// 8. Run profiling\n// ============================================\n\nconst profilingSession = Effect.gen(function* () {\n yield* Effect.log(\"=== Profiling Session ===\")\n\n yield* withMemoryLogging(\n benchmark(\"my-operation\", someEffect, 50)\n )\n\n yield* profileConcurrency\n})\n\nEffect.runPromise(profilingSession)\n```", - "antiPattern": "", - "explanation": "Profiling helps you:\n\n1. **Find bottlenecks** - What's slow?\n2. **Optimize hot paths** - Focus effort where it matters\n3. **Track regressions** - Catch slowdowns early\n4. **Right-size resources** - Don't over-provision\n\n---", - "content": "## Guideline\n\nProfile Effect applications using built-in timing spans, metrics, and Node.js profiling tools.\n\n---\n\n## Rationale\n\nProfiling helps you:\n\n1. **Find bottlenecks** - What's slow?\n2. **Optimize hot paths** - Focus effort where it matters\n3. **Track regressions** - Catch slowdowns early\n4. **Right-size resources** - Don't over-provision\n\n---\n\n## Good Example\n\n### 1. Basic Timing with Spans\n\n```typescript\nimport { Effect, Duration } from \"effect\"\n\n// ============================================\n// 1. Time individual operations\n// ============================================\n\nconst timeOperation = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n const result = yield* effect\n\n const duration = Date.now() - startTime\n yield* Effect.log(`${name}: ${duration}ms`)\n\n return result\n })\n\n// Usage\nconst program = Effect.gen(function* () {\n yield* timeOperation(\"database-query\", queryDatabase())\n yield* timeOperation(\"api-call\", callExternalApi())\n yield* timeOperation(\"processing\", processData())\n})\n\n// ============================================\n// 2. Use withLogSpan for nested timing\n// ============================================\n\nconst timedProgram = Effect.gen(function* () {\n yield* Effect.log(\"Starting\")\n\n yield* fetchUsers().pipe(Effect.withLogSpan(\"fetchUsers\"))\n\n yield* processUsers().pipe(Effect.withLogSpan(\"processUsers\"))\n\n yield* saveResults().pipe(Effect.withLogSpan(\"saveResults\"))\n\n yield* Effect.log(\"Complete\")\n}).pipe(Effect.withLogSpan(\"total\"))\n\n// ============================================\n// 3. Collect timing metrics\n// ============================================\n\nimport { Metric } from \"effect\"\n\nconst operationDuration = Metric.histogram(\"operation_duration_ms\", {\n description: \"Operation duration in milliseconds\",\n boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000],\n})\n\nconst profiledEffect = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n const startTime = Date.now()\n\n const result = yield* effect\n\n const duration = Date.now() - startTime\n yield* Metric.update(\n operationDuration.pipe(Metric.tagged(\"operation\", name)),\n duration\n )\n\n return result\n })\n\n// ============================================\n// 4. Memory profiling\n// ============================================\n\nconst logMemoryUsage = Effect.sync(() => {\n const usage = process.memoryUsage()\n return {\n heapUsed: Math.round(usage.heapUsed / 1024 / 1024),\n heapTotal: Math.round(usage.heapTotal / 1024 / 1024),\n external: Math.round(usage.external / 1024 / 1024),\n rss: Math.round(usage.rss / 1024 / 1024),\n }\n})\n\nconst withMemoryLogging = (effect: Effect.Effect) =>\n Effect.gen(function* () {\n const before = yield* logMemoryUsage\n yield* Effect.log(`Memory before: ${JSON.stringify(before)}MB`)\n\n const result = yield* effect\n\n const after = yield* logMemoryUsage\n yield* Effect.log(`Memory after: ${JSON.stringify(after)}MB`)\n yield* Effect.log(`Memory delta: ${after.heapUsed - before.heapUsed}MB`)\n\n return result\n })\n\n// ============================================\n// 5. CPU profiling with Node.js inspector\n// ============================================\n\nconst withCpuProfile = (\n name: string,\n effect: Effect.Effect\n) =>\n Effect.gen(function* () {\n // Start CPU profiler (requires --inspect flag)\n const inspector = yield* Effect.try(() => {\n const { Session } = require(\"inspector\")\n const session = new Session()\n session.connect()\n return session\n })\n\n yield* Effect.try(() => {\n inspector.post(\"Profiler.enable\")\n inspector.post(\"Profiler.start\")\n })\n\n const result = yield* effect\n\n // Stop and save profile\n yield* Effect.async((resume) => {\n inspector.post(\"Profiler.stop\", (err: Error, { profile }: any) => {\n if (err) {\n resume(Effect.fail(err))\n } else {\n const fs = require(\"fs\")\n fs.writeFileSync(\n `${name}-${Date.now()}.cpuprofile`,\n JSON.stringify(profile)\n )\n resume(Effect.void)\n }\n })\n })\n\n return result\n })\n\n// ============================================\n// 6. Benchmark specific operations\n// ============================================\n\nconst benchmark = (\n name: string,\n effect: Effect.Effect,\n iterations: number = 100\n) =>\n Effect.gen(function* () {\n const times: number[] = []\n\n for (let i = 0; i < iterations; i++) {\n const start = performance.now()\n yield* effect\n times.push(performance.now() - start)\n }\n\n const sorted = times.sort((a, b) => a - b)\n const stats = {\n min: sorted[0],\n max: sorted[sorted.length - 1],\n median: sorted[Math.floor(sorted.length / 2)],\n p95: sorted[Math.floor(sorted.length * 0.95)],\n p99: sorted[Math.floor(sorted.length * 0.99)],\n mean: times.reduce((a, b) => a + b, 0) / times.length,\n }\n\n yield* Effect.log(`Benchmark \"${name}\" (${iterations} iterations):`)\n yield* Effect.log(` Min: ${stats.min.toFixed(2)}ms`)\n yield* Effect.log(` Max: ${stats.max.toFixed(2)}ms`)\n yield* Effect.log(` Mean: ${stats.mean.toFixed(2)}ms`)\n yield* Effect.log(` Median: ${stats.median.toFixed(2)}ms`)\n yield* Effect.log(` P95: ${stats.p95.toFixed(2)}ms`)\n yield* Effect.log(` P99: ${stats.p99.toFixed(2)}ms`)\n\n return stats\n })\n\n// ============================================\n// 7. Profile concurrent operations\n// ============================================\n\nconst profileConcurrency = Effect.gen(function* () {\n const items = Array.from({ length: 100 }, (_, i) => i)\n\n // Sequential\n yield* benchmark(\n \"sequential\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 1 }),\n 10\n )\n\n // Parallel unbounded\n yield* benchmark(\n \"parallel-unbounded\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), {\n concurrency: \"unbounded\",\n }),\n 10\n )\n\n // Parallel limited\n yield* benchmark(\n \"parallel-10\",\n Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 10 }),\n 10\n )\n})\n\n// ============================================\n// 8. Run profiling\n// ============================================\n\nconst profilingSession = Effect.gen(function* () {\n yield* Effect.log(\"=== Profiling Session ===\")\n\n yield* withMemoryLogging(\n benchmark(\"my-operation\", someEffect, 50)\n )\n\n yield* profileConcurrency\n})\n\nEffect.runPromise(profilingSession)\n```\n\n## Profiling Output Example\n\n```\nBenchmark \"my-operation\" (50 iterations):\n Min: 1.23ms\n Max: 15.67ms\n Mean: 3.45ms\n Median: 2.89ms\n P95: 8.12ms\n P99: 12.34ms\n```\n\n## Profiling Tools\n\n| Tool | Use For |\n|------|---------|\n| `withLogSpan` | Basic timing |\n| `Metric.histogram` | Distribution tracking |\n| `process.memoryUsage` | Memory profiling |\n| Node.js Inspector | CPU profiling |\n| `benchmark()` | Micro-benchmarks |\n\n## Best Practices\n\n1. **Profile in production-like env** - Dev differs from prod\n2. **Warm up first** - JIT compilation affects early runs\n3. **Use percentiles** - Mean hides outliers\n4. **Profile before optimizing** - Don't guess\n5. **Track over time** - Catch regressions" - }, - { - "id": "testing-property-based", - "title": "Property-Based Testing with Effect", - "description": "Use property-based testing to find edge cases your example-based tests miss.", - "skillLevel": "advanced", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Option, Either, Schema } from \"effect\"\nimport * as fc from \"fast-check\"\n\ndescribe(\"Property-Based Testing with Effect\", () => {\n // ============================================\n // 1. Test pure function properties\n // ============================================\n\n it(\"should satisfy array reverse properties\", () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n // Reversing twice returns original\n const reversed = arr.slice().reverse()\n const doubleReversed = reversed.slice().reverse()\n\n return JSON.stringify(arr) === JSON.stringify(doubleReversed)\n })\n )\n })\n\n it(\"should satisfy sort idempotence\", () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n const sorted = arr.slice().sort((a, b) => a - b)\n const sortedTwice = sorted.slice().sort((a, b) => a - b)\n\n return JSON.stringify(sorted) === JSON.stringify(sortedTwice)\n })\n )\n })\n\n // ============================================\n // 2. Test Effect operations\n // ============================================\n\n it(\"should map then flatMap equals flatMap with mapping\", async () => {\n await fc.assert(\n fc.asyncProperty(fc.integer(), async (n) => {\n const f = (x: number) => x * 2\n const g = (x: number) => Effect.succeed(x + 1)\n\n // map then flatMap\n const result1 = await Effect.runPromise(\n Effect.succeed(n).pipe(\n Effect.map(f),\n Effect.flatMap(g)\n )\n )\n\n // flatMap with mapping inside\n const result2 = await Effect.runPromise(\n Effect.succeed(n).pipe(\n Effect.flatMap((x) => g(f(x)))\n )\n )\n\n return result1 === result2\n })\n )\n })\n\n // ============================================\n // 3. Test Option properties\n // ============================================\n\n it(\"should satisfy Option map identity\", () => {\n fc.assert(\n fc.property(fc.option(fc.integer(), { nil: undefined }), (maybeN) => {\n const option = maybeN === undefined ? Option.none() : Option.some(maybeN)\n\n // Mapping identity function returns same Option\n const mapped = Option.map(option, (x) => x)\n\n return Option.getOrElse(option, () => -1) ===\n Option.getOrElse(mapped, () => -1)\n })\n )\n })\n\n // ============================================\n // 4. Test Schema encode/decode roundtrip\n // ============================================\n\n it(\"should roundtrip through Schema\", async () => {\n const UserSchema = Schema.Struct({\n name: Schema.String,\n age: Schema.Number.pipe(Schema.int(), Schema.positive()),\n })\n\n const userArbitrary = fc.record({\n name: fc.string({ minLength: 1 }),\n age: fc.integer({ min: 1, max: 120 }),\n })\n\n await fc.assert(\n fc.asyncProperty(userArbitrary, async (user) => {\n const encode = Schema.encode(UserSchema)\n const decode = Schema.decode(UserSchema)\n\n // Encode then decode should return equivalent value\n const encoded = await Effect.runPromise(encode(user))\n const decoded = await Effect.runPromise(decode(encoded))\n\n return decoded.name === user.name && decoded.age === user.age\n })\n )\n })\n\n // ============================================\n // 5. Test error handling properties\n // ============================================\n\n it(\"should recover from any error\", async () => {\n await fc.assert(\n fc.asyncProperty(\n fc.string(),\n fc.string(),\n async (errorMsg, fallback) => {\n const failing = Effect.fail(new Error(errorMsg))\n\n const result = await Effect.runPromise(\n failing.pipe(\n Effect.catchAll(() => Effect.succeed(fallback))\n )\n )\n\n return result === fallback\n }\n )\n )\n })\n\n // ============================================\n // 6. Custom generators for domain types\n // ============================================\n\n interface Email {\n readonly _tag: \"Email\"\n readonly value: string\n }\n\n const emailArbitrary = fc.emailAddress().map((value): Email => ({\n _tag: \"Email\",\n value,\n }))\n\n interface UserId {\n readonly _tag: \"UserId\"\n readonly value: string\n }\n\n const userIdArbitrary = fc.uuid().map((value): UserId => ({\n _tag: \"UserId\",\n value,\n }))\n\n it(\"should handle domain types correctly\", () => {\n fc.assert(\n fc.property(emailArbitrary, userIdArbitrary, (email, userId) => {\n // Test your domain functions with generated domain types\n return email.value.includes(\"@\") && userId.value.length > 0\n })\n )\n })\n\n // ============================================\n // 7. Test algebraic properties\n // ============================================\n\n it(\"should satisfy monoid properties for string concat\", () => {\n const empty = \"\"\n const concat = (a: string, b: string) => a + b\n\n fc.assert(\n fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {\n // Identity: empty + a = a = a + empty\n const leftIdentity = concat(empty, a) === a\n const rightIdentity = concat(a, empty) === a\n\n // Associativity: (a + b) + c = a + (b + c)\n const associative = concat(concat(a, b), c) === concat(a, concat(b, c))\n\n return leftIdentity && rightIdentity && associative\n })\n )\n })\n\n // ============================================\n // 8. Test with constraints\n // ============================================\n\n it(\"should handle positive numbers\", () => {\n fc.assert(\n fc.property(\n fc.integer({ min: 1, max: 1000000 }),\n fc.integer({ min: 1, max: 1000000 }),\n (a, b) => {\n // Division of positives is positive\n const result = a / b\n return result > 0\n }\n )\n )\n })\n})\n```", - "antiPattern": "", - "explanation": "Property-based testing finds bugs that example tests miss:\n\n1. **Edge cases** - Empty arrays, negative numbers, unicode\n2. **Invariants** - Properties that should always hold\n3. **Shrinking** - Minimal failing examples\n4. **Coverage** - Many inputs from one test\n\n---", - "content": "## Guideline\n\nUse property-based testing with fast-check to test invariants and find edge cases automatically.\n\n---\n\n## Rationale\n\nProperty-based testing finds bugs that example tests miss:\n\n1. **Edge cases** - Empty arrays, negative numbers, unicode\n2. **Invariants** - Properties that should always hold\n3. **Shrinking** - Minimal failing examples\n4. **Coverage** - Many inputs from one test\n\n---\n\n## Good Example\n\n```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Option, Either, Schema } from \"effect\"\nimport * as fc from \"fast-check\"\n\ndescribe(\"Property-Based Testing with Effect\", () => {\n // ============================================\n // 1. Test pure function properties\n // ============================================\n\n it(\"should satisfy array reverse properties\", () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n // Reversing twice returns original\n const reversed = arr.slice().reverse()\n const doubleReversed = reversed.slice().reverse()\n\n return JSON.stringify(arr) === JSON.stringify(doubleReversed)\n })\n )\n })\n\n it(\"should satisfy sort idempotence\", () => {\n fc.assert(\n fc.property(fc.array(fc.integer()), (arr) => {\n const sorted = arr.slice().sort((a, b) => a - b)\n const sortedTwice = sorted.slice().sort((a, b) => a - b)\n\n return JSON.stringify(sorted) === JSON.stringify(sortedTwice)\n })\n )\n })\n\n // ============================================\n // 2. Test Effect operations\n // ============================================\n\n it(\"should map then flatMap equals flatMap with mapping\", async () => {\n await fc.assert(\n fc.asyncProperty(fc.integer(), async (n) => {\n const f = (x: number) => x * 2\n const g = (x: number) => Effect.succeed(x + 1)\n\n // map then flatMap\n const result1 = await Effect.runPromise(\n Effect.succeed(n).pipe(\n Effect.map(f),\n Effect.flatMap(g)\n )\n )\n\n // flatMap with mapping inside\n const result2 = await Effect.runPromise(\n Effect.succeed(n).pipe(\n Effect.flatMap((x) => g(f(x)))\n )\n )\n\n return result1 === result2\n })\n )\n })\n\n // ============================================\n // 3. Test Option properties\n // ============================================\n\n it(\"should satisfy Option map identity\", () => {\n fc.assert(\n fc.property(fc.option(fc.integer(), { nil: undefined }), (maybeN) => {\n const option = maybeN === undefined ? Option.none() : Option.some(maybeN)\n\n // Mapping identity function returns same Option\n const mapped = Option.map(option, (x) => x)\n\n return Option.getOrElse(option, () => -1) ===\n Option.getOrElse(mapped, () => -1)\n })\n )\n })\n\n // ============================================\n // 4. Test Schema encode/decode roundtrip\n // ============================================\n\n it(\"should roundtrip through Schema\", async () => {\n const UserSchema = Schema.Struct({\n name: Schema.String,\n age: Schema.Number.pipe(Schema.int(), Schema.positive()),\n })\n\n const userArbitrary = fc.record({\n name: fc.string({ minLength: 1 }),\n age: fc.integer({ min: 1, max: 120 }),\n })\n\n await fc.assert(\n fc.asyncProperty(userArbitrary, async (user) => {\n const encode = Schema.encode(UserSchema)\n const decode = Schema.decode(UserSchema)\n\n // Encode then decode should return equivalent value\n const encoded = await Effect.runPromise(encode(user))\n const decoded = await Effect.runPromise(decode(encoded))\n\n return decoded.name === user.name && decoded.age === user.age\n })\n )\n })\n\n // ============================================\n // 5. Test error handling properties\n // ============================================\n\n it(\"should recover from any error\", async () => {\n await fc.assert(\n fc.asyncProperty(\n fc.string(),\n fc.string(),\n async (errorMsg, fallback) => {\n const failing = Effect.fail(new Error(errorMsg))\n\n const result = await Effect.runPromise(\n failing.pipe(\n Effect.catchAll(() => Effect.succeed(fallback))\n )\n )\n\n return result === fallback\n }\n )\n )\n })\n\n // ============================================\n // 6. Custom generators for domain types\n // ============================================\n\n interface Email {\n readonly _tag: \"Email\"\n readonly value: string\n }\n\n const emailArbitrary = fc.emailAddress().map((value): Email => ({\n _tag: \"Email\",\n value,\n }))\n\n interface UserId {\n readonly _tag: \"UserId\"\n readonly value: string\n }\n\n const userIdArbitrary = fc.uuid().map((value): UserId => ({\n _tag: \"UserId\",\n value,\n }))\n\n it(\"should handle domain types correctly\", () => {\n fc.assert(\n fc.property(emailArbitrary, userIdArbitrary, (email, userId) => {\n // Test your domain functions with generated domain types\n return email.value.includes(\"@\") && userId.value.length > 0\n })\n )\n })\n\n // ============================================\n // 7. Test algebraic properties\n // ============================================\n\n it(\"should satisfy monoid properties for string concat\", () => {\n const empty = \"\"\n const concat = (a: string, b: string) => a + b\n\n fc.assert(\n fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {\n // Identity: empty + a = a = a + empty\n const leftIdentity = concat(empty, a) === a\n const rightIdentity = concat(a, empty) === a\n\n // Associativity: (a + b) + c = a + (b + c)\n const associative = concat(concat(a, b), c) === concat(a, concat(b, c))\n\n return leftIdentity && rightIdentity && associative\n })\n )\n })\n\n // ============================================\n // 8. Test with constraints\n // ============================================\n\n it(\"should handle positive numbers\", () => {\n fc.assert(\n fc.property(\n fc.integer({ min: 1, max: 1000000 }),\n fc.integer({ min: 1, max: 1000000 }),\n (a, b) => {\n // Division of positives is positive\n const result = a / b\n return result > 0\n }\n )\n )\n })\n})\n```\n\n## Setup\n\n```bash\nbun add -D fast-check\n```\n\n## Useful Arbitraries\n\n| Arbitrary | Generates |\n|-----------|-----------|\n| `fc.integer()` | Integers |\n| `fc.string()` | Strings |\n| `fc.array(arb)` | Arrays |\n| `fc.record({})` | Objects |\n| `fc.option(arb)` | Optional values |\n| `fc.uuid()` | UUIDs |\n| `fc.emailAddress()` | Emails |\n| `fc.date()` | Dates |\n\n## Properties to Test\n\n| Property | Example |\n|----------|---------|\n| **Roundtrip** | decode(encode(x)) === x |\n| **Idempotence** | f(f(x)) === f(x) |\n| **Commutativity** | f(a,b) === f(b,a) |\n| **Associativity** | f(f(a,b),c) === f(a,f(b,c)) |\n| **Identity** | f(x, id) === x |\n\n## Best Practices\n\n1. **Start simple** - Basic properties first\n2. **Use constraints** - Limit input ranges\n3. **Custom generators** - For domain types\n4. **Read shrunk examples** - Understand failures\n5. **Combine with examples** - Not a replacement" - }, - { - "id": "provide-config-layer", - "title": "Provide Configuration to Your App via a Layer", - "description": "Provide configuration to your app via a Layer.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Layer } from \"effect\";\n\nclass ServerConfig extends Effect.Service()(\"ServerConfig\", {\n sync: () => ({\n port: process.env.PORT ? parseInt(process.env.PORT) : 8080,\n }),\n}) {}\n\nconst program = Effect.gen(function* () {\n const config = yield* ServerConfig;\n yield* Effect.log(`Starting application on port ${config.port}...`);\n});\n\nconst programWithErrorHandling = Effect.provide(\n program,\n ServerConfig.Default\n).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n**Explanation:** \nThis approach makes configuration available contextually, supporting better testing and modularity.", - "antiPattern": "Manually reading environment variables deep inside business logic. This tightly couples that logic to the external environment, making it difficult to test and reuse.", - "explanation": "Integrating configuration as a `Layer` plugs it directly into Effect's dependency injection system. This makes your configuration available anywhere in the program and dramatically simplifies testing by allowing you to substitute mock configuration.", - "content": "# Provide Configuration to Your App via a Layer\n\n## Guideline\n\nTransform your configuration schema into a `Layer` using `Config.layer()` and provide it to your main application `Effect`.\n\n## Rationale\n\nIntegrating configuration as a `Layer` plugs it directly into Effect's dependency injection system. This makes your configuration available anywhere in the program and dramatically simplifies testing by allowing you to substitute mock configuration.\n\n## Good Example\n\n```typescript\nimport { Effect, Layer } from \"effect\";\n\nclass ServerConfig extends Effect.Service()(\"ServerConfig\", {\n sync: () => ({\n port: process.env.PORT ? parseInt(process.env.PORT) : 8080,\n }),\n}) {}\n\nconst program = Effect.gen(function* () {\n const config = yield* ServerConfig;\n yield* Effect.log(`Starting application on port ${config.port}...`);\n});\n\nconst programWithErrorHandling = Effect.provide(\n program,\n ServerConfig.Default\n).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithErrorHandling);\n```\n\n**Explanation:** \nThis approach makes configuration available contextually, supporting better testing and modularity.\n\n## Anti-Pattern\n\nManually reading environment variables deep inside business logic. This tightly couples that logic to the external environment, making it difficult to test and reuse." - }, - { - "id": "provide-dependencies-to-routes", - "title": "Provide Dependencies to Routes", - "description": "Define dependencies with Effect.Service and provide them to your HTTP server using a Layer.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "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`.\n\n```typescript\nimport * as HttpRouter from \"@effect/platform/HttpRouter\";\nimport * as HttpResponse from \"@effect/platform/HttpServerResponse\";\nimport * as HttpServer from \"@effect/platform/HttpServer\";\nimport { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Duration, Fiber } from \"effect/index\";\nimport { Data } from \"effect\";\n\n// 1. Define the service interface using Effect.Service\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: string) =>\n id === \"123\"\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError({ id })),\n }),\n}) {}\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n\n// handler producing a `HttpServerResponse`\nconst userHandler = Effect.flatMap(HttpRouter.params, (p) =>\n Effect.flatMap(Database, (db) => db.getUser(p[\"userId\"] ?? \"\")).pipe(\n Effect.flatMap(HttpResponse.json)\n )\n);\n\n// assemble router & server\nconst app = HttpRouter.empty.pipe(\n HttpRouter.get(\"/users/:userId\", userHandler)\n);\n\n// Create the server effect with all dependencies\nconst serverEffect = HttpServer.serveEffect(app).pipe(\n Effect.provide(Database.Default),\n Effect.provide(\n NodeHttpServer.layer(() => require(\"node:http\").createServer(), {\n port: 3458,\n })\n )\n);\n\n// Create program that manages server lifecycle\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting server on port 3458...\");\n\n const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.fork);\n\n yield* Effect.logInfo(\"Server started successfully on http://localhost:3458\");\n yield* Effect.logInfo(\"Try: curl http://localhost:3458/users/123\");\n yield* Effect.logInfo(\"Try: curl http://localhost:3458/users/456\");\n\n // Run for a short time to demonstrate\n yield* Effect.sleep(Duration.seconds(3));\n\n yield* Effect.logInfo(\"Shutting down server...\");\n yield* Fiber.interrupt(serverFiber);\n yield* Effect.logInfo(\"Server shutdown complete\");\n});\n\n// Run the program\nNodeRuntime.runMain(program);\n```", - "antiPattern": "The anti-pattern is to manually instantiate and pass dependencies through function arguments. This creates tight coupling and makes testing difficult.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// Manual implementation of a database client\nclass LiveDatabase {\n getUser(id: string) {\n if (id === \"123\") {\n return Effect.succeed({ name: \"Paul\" });\n }\n return Effect.fail(\"User not found\"); // Untyped error\n }\n}\n\n// The dependency must be passed explicitly to the route definition\nconst createGetUserRoute = (db: LiveDatabase) =>\n Http.router.get(\n \"/users/:userId\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n db.getUser(req.params.userId)\n ).pipe(\n Effect.map(Http.response.json),\n Effect.catchAll(() => Http.response.empty({ status: 404 }))\n )\n );\n\n// Manually instantiate the dependency\nconst db = new LiveDatabase();\nconst getUserRoute = createGetUserRoute(db);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis approach is flawed because the route handler is now aware of the concrete `LiveDatabase` class. Swapping it for a mock in a test would be cumbersome. Furthermore, if a service deep within the call stack needs a dependency, it must be \"drilled\" down through every intermediate function, which is a significant maintenance burden.", - "explanation": "As applications grow, route handlers need to perform complex tasks like accessing a database, calling other APIs, or logging. Hard-coding this logic or manually passing dependencies leads to tightly coupled, untestable code.\n\nEffect's dependency injection system (`Service` and `Layer`) solves this by decoupling a service's interface from its implementation. This is the cornerstone of building scalable, maintainable applications in Effect.\n\n1. **Modern and Simple**: `Effect.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate.\n2. **Testability**: By depending on a service interface, you can easily provide a mock implementation in your tests (e.g., `Database.Test`) instead of the real one (`Database.Live`), allowing for fast, isolated unit tests of your route logic.\n3. **Decoupling**: Route handlers don't know or care _how_ the database connection is created or managed. They simply ask for the `Database` service from the context, and the runtime provides the configured implementation.\n4. **Composability**: `Layer`s are composable. You can build complex dependency graphs (e.g., a `Database` layer that itself requires a `Config` layer) that Effect will automatically construct and wire up for you.\n\n---", - "content": "## Guideline\n\nDefine your application's services using `class MyService extends Effect.Service(\"MyService\")`, provide a live implementation via a `Layer`, and use `Effect.provide` to make the service available to your entire HTTP application.\n\n---\n\n## Rationale\n\nAs applications grow, route handlers need to perform complex tasks like accessing a database, calling other APIs, or logging. Hard-coding this logic or manually passing dependencies leads to tightly coupled, untestable code.\n\nEffect's dependency injection system (`Service` and `Layer`) solves this by decoupling a service's interface from its implementation. This is the cornerstone of building scalable, maintainable applications in Effect.\n\n1. **Modern and Simple**: `Effect.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate.\n2. **Testability**: By depending on a service interface, you can easily provide a mock implementation in your tests (e.g., `Database.Test`) instead of the real one (`Database.Live`), allowing for fast, isolated unit tests of your route logic.\n3. **Decoupling**: Route handlers don't know or care _how_ the database connection is created or managed. They simply ask for the `Database` service from the context, and the runtime provides the configured implementation.\n4. **Composability**: `Layer`s are composable. You can build complex dependency graphs (e.g., a `Database` layer that itself requires a `Config` layer) that Effect will automatically construct and wire up for you.\n\n---\n\n## Good Example\n\nThis 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`.\n\n```typescript\nimport * as HttpRouter from \"@effect/platform/HttpRouter\";\nimport * as HttpResponse from \"@effect/platform/HttpServerResponse\";\nimport * as HttpServer from \"@effect/platform/HttpServer\";\nimport { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\nimport { Effect, Duration, Fiber } from \"effect/index\";\nimport { Data } from \"effect\";\n\n// 1. Define the service interface using Effect.Service\nexport class Database extends Effect.Service()(\"Database\", {\n sync: () => ({\n getUser: (id: string) =>\n id === \"123\"\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError({ id })),\n }),\n}) {}\n\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\")<{\n id: string;\n}> {}\n\n// handler producing a `HttpServerResponse`\nconst userHandler = Effect.flatMap(HttpRouter.params, (p) =>\n Effect.flatMap(Database, (db) => db.getUser(p[\"userId\"] ?? \"\")).pipe(\n Effect.flatMap(HttpResponse.json)\n )\n);\n\n// assemble router & server\nconst app = HttpRouter.empty.pipe(\n HttpRouter.get(\"/users/:userId\", userHandler)\n);\n\n// Create the server effect with all dependencies\nconst serverEffect = HttpServer.serveEffect(app).pipe(\n Effect.provide(Database.Default),\n Effect.provide(\n NodeHttpServer.layer(() => require(\"node:http\").createServer(), {\n port: 3458,\n })\n )\n);\n\n// Create program that manages server lifecycle\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting server on port 3458...\");\n\n const serverFiber = yield* Effect.scoped(serverEffect).pipe(Effect.fork);\n\n yield* Effect.logInfo(\"Server started successfully on http://localhost:3458\");\n yield* Effect.logInfo(\"Try: curl http://localhost:3458/users/123\");\n yield* Effect.logInfo(\"Try: curl http://localhost:3458/users/456\");\n\n // Run for a short time to demonstrate\n yield* Effect.sleep(Duration.seconds(3));\n\n yield* Effect.logInfo(\"Shutting down server...\");\n yield* Fiber.interrupt(serverFiber);\n yield* Effect.logInfo(\"Server shutdown complete\");\n});\n\n// Run the program\nNodeRuntime.runMain(program);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to manually instantiate and pass dependencies through function arguments. This creates tight coupling and makes testing difficult.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\n// Manual implementation of a database client\nclass LiveDatabase {\n getUser(id: string) {\n if (id === \"123\") {\n return Effect.succeed({ name: \"Paul\" });\n }\n return Effect.fail(\"User not found\"); // Untyped error\n }\n}\n\n// The dependency must be passed explicitly to the route definition\nconst createGetUserRoute = (db: LiveDatabase) =>\n Http.router.get(\n \"/users/:userId\",\n Effect.flatMap(Http.request.ServerRequest, (req) =>\n db.getUser(req.params.userId)\n ).pipe(\n Effect.map(Http.response.json),\n Effect.catchAll(() => Http.response.empty({ status: 404 }))\n )\n );\n\n// Manually instantiate the dependency\nconst db = new LiveDatabase();\nconst getUserRoute = createGetUserRoute(db);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis approach is flawed because the route handler is now aware of the concrete `LiveDatabase` class. Swapping it for a mock in a test would be cumbersome. Furthermore, if a service deep within the call stack needs a dependency, it must be \"drilled\" down through every intermediate function, which is a significant maintenance burden." - }, - { - "id": "race-concurrent-effects", - "title": "Race Concurrent Effects for the Fastest Result", - "description": "Use Effect.race to get the result from the first of several effects to succeed, automatically interrupting the losers.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "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.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ntype User = { id: number; name: string };\n\n// Simulate a slower cache lookup that might find nothing (None)\nconst checkCache: Effect.Effect> = Effect.succeed(\n Option.none()\n).pipe(\n Effect.delay(\"200 millis\") // Made slower so database wins\n);\n\n// Simulate a faster database query that will always find the data\nconst queryDatabase: Effect.Effect> = Effect.succeed(\n Option.some({ id: 1, name: \"Paul\" })\n).pipe(\n Effect.delay(\"50 millis\") // Made faster so it wins the race\n);\n\n// Race them. The database should win and return the user data.\nconst program = Effect.race(checkCache, queryDatabase).pipe(\n // The result of the race is an Option, so we can handle it.\n Effect.flatMap((result: Option.Option) =>\n Option.match(result, {\n onNone: () => Effect.fail(\"User not found anywhere.\"),\n onSome: (user) => Effect.succeed(user),\n })\n )\n);\n\n// In this case, the database wins the race.\nconst programWithResults = Effect.gen(function* () {\n try {\n const user = yield* program;\n yield* Effect.log(`User found: ${JSON.stringify(user)}`);\n return user;\n } catch (error) {\n yield* Effect.logError(`Error: ${error}`);\n throw error;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Handled error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithResults);\n\n// Also demonstrate with logging\nconst programWithLogging = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting race between cache and database...\");\n\n try {\n const user = yield* program;\n yield* Effect.logInfo(\n `Success: Found user ${user.name} with ID ${user.id}`\n );\n return user;\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n return null;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Handled error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "Don't use `Effect.race` if you need the results of _all_ the effects. That is the job of `Effect.all`. Using `race` in this scenario will cause you to lose data, as all but one of the effects will be interrupted and their results discarded.\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst fetchProfile = Effect.succeed({ name: \"Paul\" });\nconst fetchPermissions = Effect.succeed([\"admin\", \"editor\"]);\n\n// ❌ WRONG: This will only return either the profile OR the permissions,\n// whichever resolves first. You will lose the other piece of data.\nconst incompleteData = Effect.race(fetchProfile, fetchPermissions);\n\n// ✅ CORRECT: Use Effect.all when you need all the results.\nconst completeData = Effect.all([fetchProfile, fetchPermissions]);\n```", - "explanation": "`Effect.race` is a powerful concurrency primitive for performance and resilience. It starts all provided effects in parallel. The moment one of them succeeds, `Effect.race` immediately interrupts all the other \"losing\" effects and returns the winning result. If one of the effects fails before any have succeeded, the race is not over; the remaining effects continue to run. The entire race only fails if _all_ participating effects fail.\n\nThis is commonly used for:\n\n- **Performance:** Querying multiple redundant data sources (e.g., two API replicas) and taking the response from whichever is faster.\n- **Implementing Timeouts:** Racing a primary effect against a delayed `Effect.fail`, effectively creating a timeout mechanism.\n\n---", - "content": "## Guideline\n\nWhen you have multiple effects that can produce the same type of result, and you only care about the one that finishes first, use `Effect.race(effectA, effectB)`.\n\n---\n\n## Rationale\n\n`Effect.race` is a powerful concurrency primitive for performance and resilience. It starts all provided effects in parallel. The moment one of them succeeds, `Effect.race` immediately interrupts all the other \"losing\" effects and returns the winning result. If one of the effects fails before any have succeeded, the race is not over; the remaining effects continue to run. The entire race only fails if _all_ participating effects fail.\n\nThis is commonly used for:\n\n- **Performance:** Querying multiple redundant data sources (e.g., two API replicas) and taking the response from whichever is faster.\n- **Implementing Timeouts:** Racing a primary effect against a delayed `Effect.fail`, effectively creating a timeout mechanism.\n\n---\n\n## Good Example\n\nA 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.\n\n```typescript\nimport { Effect, Option } from \"effect\";\n\ntype User = { id: number; name: string };\n\n// Simulate a slower cache lookup that might find nothing (None)\nconst checkCache: Effect.Effect> = Effect.succeed(\n Option.none()\n).pipe(\n Effect.delay(\"200 millis\") // Made slower so database wins\n);\n\n// Simulate a faster database query that will always find the data\nconst queryDatabase: Effect.Effect> = Effect.succeed(\n Option.some({ id: 1, name: \"Paul\" })\n).pipe(\n Effect.delay(\"50 millis\") // Made faster so it wins the race\n);\n\n// Race them. The database should win and return the user data.\nconst program = Effect.race(checkCache, queryDatabase).pipe(\n // The result of the race is an Option, so we can handle it.\n Effect.flatMap((result: Option.Option) =>\n Option.match(result, {\n onNone: () => Effect.fail(\"User not found anywhere.\"),\n onSome: (user) => Effect.succeed(user),\n })\n )\n);\n\n// In this case, the database wins the race.\nconst programWithResults = Effect.gen(function* () {\n try {\n const user = yield* program;\n yield* Effect.log(`User found: ${JSON.stringify(user)}`);\n return user;\n } catch (error) {\n yield* Effect.logError(`Error: ${error}`);\n throw error;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Handled error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithResults);\n\n// Also demonstrate with logging\nconst programWithLogging = Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting race between cache and database...\");\n\n try {\n const user = yield* program;\n yield* Effect.logInfo(\n `Success: Found user ${user.name} with ID ${user.id}`\n );\n return user;\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n return null;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Handled error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nDon't use `Effect.race` if you need the results of _all_ the effects. That is the job of `Effect.all`. Using `race` in this scenario will cause you to lose data, as all but one of the effects will be interrupted and their results discarded.\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst fetchProfile = Effect.succeed({ name: \"Paul\" });\nconst fetchPermissions = Effect.succeed([\"admin\", \"editor\"]);\n\n// ❌ WRONG: This will only return either the profile OR the permissions,\n// whichever resolves first. You will lose the other piece of data.\nconst incompleteData = Effect.race(fetchProfile, fetchPermissions);\n\n// ✅ CORRECT: Use Effect.all when you need all the results.\nconst completeData = Effect.all([fetchProfile, fetchPermissions]);\n```" - }, - { - "id": "concurrency-race-timeout", - "title": "Race Effects and Handle Timeouts", - "description": "Use Effect.race for fastest-wins, Effect.timeout for time limits.", - "skillLevel": "beginner", - "useCases": [ - "concurrency-getting-started" - ], - "example": "```typescript\nimport { Effect, Option } from \"effect\"\n\n// ============================================\n// BASIC RACE: First one wins\n// ============================================\n\nconst server1 = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return \"Response from server 1\"\n})\n\nconst server2 = Effect.gen(function* () {\n yield* Effect.sleep(\"50 millis\")\n return \"Response from server 2\"\n})\n\nconst raceServers = Effect.race(server1, server2)\n\nEffect.runPromise(raceServers).then((result) => {\n console.log(result) // \"Response from server 2\" (faster)\n})\n\n// ============================================\n// BASIC TIMEOUT: Limit execution time\n// ============================================\n\nconst slowOperation = Effect.gen(function* () {\n yield* Effect.sleep(\"5 seconds\")\n return \"Finally done\"\n})\n\n// Returns Option.none if timeout\nconst withTimeout = slowOperation.pipe(\n Effect.timeout(\"1 second\")\n)\n\nEffect.runPromise(withTimeout).then((result) => {\n if (Option.isNone(result)) {\n console.log(\"Operation timed out\")\n } else {\n console.log(`Got: ${result.value}`)\n }\n})\n\n// ============================================\n// TIMEOUT WITH FALLBACK\n// ============================================\n\nconst withFallback = slowOperation.pipe(\n Effect.timeoutTo({\n duration: \"1 second\",\n onTimeout: () => Effect.succeed(\"Using cached value\"),\n })\n)\n\nEffect.runPromise(withFallback).then((result) => {\n console.log(result) // \"Using cached value\"\n})\n\n// ============================================\n// TIMEOUT FAIL: Throw error on timeout\n// ============================================\n\nclass TimeoutError {\n readonly _tag = \"TimeoutError\"\n}\n\nconst failOnTimeout = slowOperation.pipe(\n Effect.timeoutFail({\n duration: \"1 second\",\n onTimeout: () => new TimeoutError(),\n })\n)\n\n// ============================================\n// RACE ALL: Multiple competing effects\n// ============================================\n\nconst fetchFromCache = Effect.gen(function* () {\n yield* Effect.sleep(\"10 millis\")\n return { source: \"cache\", data: \"cached data\" }\n})\n\nconst fetchFromDB = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return { source: \"db\", data: \"fresh data\" }\n})\n\nconst fetchFromAPI = Effect.gen(function* () {\n yield* Effect.sleep(\"200 millis\")\n return { source: \"api\", data: \"api data\" }\n})\n\nconst raceAll = Effect.raceAll([fetchFromCache, fetchFromDB, fetchFromAPI])\n\nEffect.runPromise(raceAll).then((result) => {\n console.log(`Winner: ${result.source}`) // \"cache\"\n})\n\n// ============================================\n// PRACTICAL: API with timeout and fallback\n// ============================================\n\nconst fetchWithResilience = (url: string) =>\n Effect.gen(function* () {\n const response = yield* Effect.tryPromise(() =>\n fetch(url).then((r) => r.json())\n ).pipe(\n Effect.timeout(\"3 seconds\"),\n Effect.flatMap((opt) =>\n Option.isSome(opt)\n ? Effect.succeed(opt.value)\n : Effect.succeed({ error: \"timeout\", cached: true })\n )\n )\n \n return response\n })\n```", - "antiPattern": "", - "explanation": "Racing and timeouts prevent your app from hanging:\n\n1. **Redundant requests** - Race multiple servers, use fastest response\n2. **Timeouts** - Fail fast if operation takes too long\n3. **Fallbacks** - Try fast path, fall back to slow path\n\n---", - "content": "## Guideline\n\nUse `Effect.race` when you want the first result from competing effects. Use `Effect.timeout` to limit how long an effect can run.\n\n---\n\n## Rationale\n\nRacing and timeouts prevent your app from hanging:\n\n1. **Redundant requests** - Race multiple servers, use fastest response\n2. **Timeouts** - Fail fast if operation takes too long\n3. **Fallbacks** - Try fast path, fall back to slow path\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Option } from \"effect\"\n\n// ============================================\n// BASIC RACE: First one wins\n// ============================================\n\nconst server1 = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return \"Response from server 1\"\n})\n\nconst server2 = Effect.gen(function* () {\n yield* Effect.sleep(\"50 millis\")\n return \"Response from server 2\"\n})\n\nconst raceServers = Effect.race(server1, server2)\n\nEffect.runPromise(raceServers).then((result) => {\n console.log(result) // \"Response from server 2\" (faster)\n})\n\n// ============================================\n// BASIC TIMEOUT: Limit execution time\n// ============================================\n\nconst slowOperation = Effect.gen(function* () {\n yield* Effect.sleep(\"5 seconds\")\n return \"Finally done\"\n})\n\n// Returns Option.none if timeout\nconst withTimeout = slowOperation.pipe(\n Effect.timeout(\"1 second\")\n)\n\nEffect.runPromise(withTimeout).then((result) => {\n if (Option.isNone(result)) {\n console.log(\"Operation timed out\")\n } else {\n console.log(`Got: ${result.value}`)\n }\n})\n\n// ============================================\n// TIMEOUT WITH FALLBACK\n// ============================================\n\nconst withFallback = slowOperation.pipe(\n Effect.timeoutTo({\n duration: \"1 second\",\n onTimeout: () => Effect.succeed(\"Using cached value\"),\n })\n)\n\nEffect.runPromise(withFallback).then((result) => {\n console.log(result) // \"Using cached value\"\n})\n\n// ============================================\n// TIMEOUT FAIL: Throw error on timeout\n// ============================================\n\nclass TimeoutError {\n readonly _tag = \"TimeoutError\"\n}\n\nconst failOnTimeout = slowOperation.pipe(\n Effect.timeoutFail({\n duration: \"1 second\",\n onTimeout: () => new TimeoutError(),\n })\n)\n\n// ============================================\n// RACE ALL: Multiple competing effects\n// ============================================\n\nconst fetchFromCache = Effect.gen(function* () {\n yield* Effect.sleep(\"10 millis\")\n return { source: \"cache\", data: \"cached data\" }\n})\n\nconst fetchFromDB = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return { source: \"db\", data: \"fresh data\" }\n})\n\nconst fetchFromAPI = Effect.gen(function* () {\n yield* Effect.sleep(\"200 millis\")\n return { source: \"api\", data: \"api data\" }\n})\n\nconst raceAll = Effect.raceAll([fetchFromCache, fetchFromDB, fetchFromAPI])\n\nEffect.runPromise(raceAll).then((result) => {\n console.log(`Winner: ${result.source}`) // \"cache\"\n})\n\n// ============================================\n// PRACTICAL: API with timeout and fallback\n// ============================================\n\nconst fetchWithResilience = (url: string) =>\n Effect.gen(function* () {\n const response = yield* Effect.tryPromise(() =>\n fetch(url).then((r) => r.json())\n ).pipe(\n Effect.timeout(\"3 seconds\"),\n Effect.flatMap((opt) =>\n Option.isSome(opt)\n ? Effect.succeed(opt.value)\n : Effect.succeed({ error: \"timeout\", cached: true })\n )\n )\n \n return response\n })\n```\n\n## Timeout Methods\n\n| Method | Behavior |\n|--------|----------|\n| `Effect.timeout(duration)` | Returns `Option`, None on timeout |\n| `Effect.timeoutTo({duration, onTimeout})` | Custom fallback effect |\n| `Effect.timeoutFail({duration, onTimeout})` | Fail with error on timeout |\n\n## Race Methods\n\n| Method | Behavior |\n|--------|----------|\n| `Effect.race(a, b)` | First of two effects |\n| `Effect.raceAll([a, b, c])` | First of many effects |\n| `Effect.raceFirst(a, b)` | First to complete (even if error) |\n\n## Common Patterns\n\n```typescript\n// Try fast, fall back to slow\nEffect.race(\n fastPath.pipe(Effect.timeout(\"100 millis\")),\n slowPath\n)\n\n// Request with hard timeout\nfetch(url).pipe(\n Effect.timeoutFail({\n duration: \"5 seconds\",\n onTimeout: () => new RequestTimeoutError()\n })\n)\n\n// Redundant requests for reliability\nEffect.raceAll([\n fetchFromServer(\"us-east\"),\n fetchFromServer(\"us-west\"),\n fetchFromServer(\"eu-west\"),\n])\n```" - }, - { - "id": "tooling-type-errors", - "title": "Read Effect Type Errors", - "description": "Effect errors are verbose but structured - learn to extract the key information.", - "skillLevel": "beginner", - "useCases": [ - "tooling-and-debugging" - ], - "example": "", - "antiPattern": "", - "explanation": "Effect's type system catches many bugs at compile time, but:\n\n1. **Effect types are complex** - Three type parameters\n2. **Errors are nested** - Multiple layers of generics\n3. **Messages are verbose** - TypeScript shows everything\n\nUnderstanding the pattern makes errors manageable.\n\n---", - "content": "## Guideline\n\nEffect type errors can be long, but they follow a pattern. Learn to scan for the key parts.\n\n---\n\n## Rationale\n\nEffect's type system catches many bugs at compile time, but:\n\n1. **Effect types are complex** - Three type parameters\n2. **Errors are nested** - Multiple layers of generics\n3. **Messages are verbose** - TypeScript shows everything\n\nUnderstanding the pattern makes errors manageable.\n\n---\n\n## Common Type Errors\n\n### 1. Missing Service Dependency\n\n**Error:**\n```\nType 'Effect' is not assignable to type 'Effect'.\n Type 'UserService' is not assignable to type 'never'.\n```\n\n**What it means:** You're using a service but not providing it.\n\n**Fix:**\n```typescript\n// Before - service not provided\nconst result = Effect.runPromise(getUser(\"123\")) // ❌\n\n// After - provide the service\nconst result = Effect.runPromise(\n getUser(\"123\").pipe(\n Effect.provideService(UserService, myUserService)\n )\n) // ✅\n```\n\n### 2. Unhandled Error Type\n\n**Error:**\n```\nType 'Effect' is not assignable to type 'Effect'.\n Type 'NotFoundError' is not assignable to type 'never'.\n```\n\n**What it means:** The effect can fail, but you're treating it as if it can't.\n\n**Fix:**\n```typescript\n// Before - error not handled\nconst result = Effect.runPromise(findUser(\"123\")) // ❌\n\n// After - handle the error\nconst result = Effect.runPromise(\n findUser(\"123\").pipe(\n Effect.catchAll(() => Effect.succeed(defaultUser))\n )\n) // ✅\n```\n\n### 3. Wrong Return Type\n\n**Error:**\n```\nType 'string' is not assignable to type 'Effect'.\n```\n\n**What it means:** You returned a plain value where an Effect was expected.\n\n**Fix:**\n```typescript\n// Before - plain return\nconst getName = () => \"Alice\" // ❌\n\n// After - wrap in Effect\nconst getName = () => Effect.succeed(\"Alice\") // ✅\n```\n\n### 4. Generator Yield Type Mismatch\n\n**Error:**\n```\nType 'Effect' is not assignable to parameter of type 'Effect'.\n```\n\n**What it means:** In a generator, one effect has an error type that others don't.\n\n**Fix:**\n```typescript\n// Before - mixing error types\nconst program = Effect.gen(function* () {\n const a = yield* Effect.succeed(1)\n const b = yield* mayFail() // Has Error type\n return a + b // ❌ Types don't match\n})\n\n// After - handle or propagate consistently\nconst program = Effect.gen(function* () {\n const a = yield* Effect.succeed(1)\n const b = yield* mayFail().pipe(\n Effect.catchAll(() => Effect.succeed(0))\n )\n return a + b // ✅\n})\n```\n\n---\n\n## Reading Effect Type Signatures\n\nEffect has three type parameters:\n\n```typescript\nEffect\n// ^ ^ ^\n// | | What services it needs\n// | What errors it can fail with\n// What it returns on success\n```\n\n### Examples:\n\n| Type | Meaning |\n|------|---------|\n| `Effect` | Returns number, can't fail, no dependencies |\n| `Effect` | Returns User, can fail with NotFoundError |\n| `Effect` | Needs Database service, returns nothing |\n| `Effect` | Needs Config and Logger, can fail |\n\n---\n\n## Tips for Reading Errors\n\n1. **Scroll to the end** - The most specific error is usually last\n2. **Look for your types** - Your domain types point to the problem\n3. **Check the three positions** - Success, Error, Requirements\n4. **Compare what is vs what's expected** - The error shows both\n5. **Use the Effect extension** - It simplifies type display\n\n---\n\n## Quick Fixes\n\n| Problem | Typical Fix |\n|---------|-------------|\n| `X is not assignable to never` (Error position) | Handle the error |\n| `X is not assignable to never` (Requirements) | Provide the service |\n| `Type 'X' is not assignable to 'Effect<...>'` | Wrap with Effect.succeed |\n| Generator type mismatch | Ensure all yields have compatible types |" - }, - { - "id": "data-redacted", - "title": "Redact and Handle Sensitive Data", - "description": "Use Redacted to wrap sensitive values, preventing accidental exposure in logs or error messages.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Redacted } from \"effect\";\n\n// Wrap a sensitive value\nconst secret = Redacted.make(\"super-secret-password\");\n\n// Use the secret in your application logic\nfunction authenticate(user: string, password: Redacted.Redacted) {\n // ... authentication logic\n}\n\n// Logging or stringifying a Redacted value\nconsole.log(`Password: ${secret}`); // Output: Password: \nconsole.log(String(secret)); // Output: \n```\n\n**Explanation:**\n\n- `Redacted.make(value)` wraps a sensitive value.\n- When logged or stringified, the value is replaced with ``.\n- Prevents accidental exposure of secrets in logs or error messages.", - "antiPattern": "Passing sensitive data as plain strings, which can be accidentally logged, serialized, or leaked in error messages.", - "explanation": "Sensitive data should never appear in logs, traces, or error messages. \n`Redacted` provides a type-safe way to mark and protect secrets throughout your application.", - "content": "# Redact and Handle Sensitive Data with `Redacted`\n\n## Guideline\n\nUse the `Redacted` data type to securely handle sensitive data such as passwords, API keys, or tokens. \n`Redacted` ensures that secrets are not accidentally logged, serialized, or exposed in error messages.\n\n## Rationale\n\nSensitive data should never appear in logs, traces, or error messages. \n`Redacted` provides a type-safe way to mark and protect secrets throughout your application.\n\n## Good Example\n\n```typescript\nimport { Redacted } from \"effect\";\n\n// Wrap a sensitive value\nconst secret = Redacted.make(\"super-secret-password\");\n\n// Use the secret in your application logic\nfunction authenticate(user: string, password: Redacted.Redacted) {\n // ... authentication logic\n}\n\n// Logging or stringifying a Redacted value\nconsole.log(`Password: ${secret}`); // Output: Password: \nconsole.log(String(secret)); // Output: \n```\n\n**Explanation:**\n\n- `Redacted.make(value)` wraps a sensitive value.\n- When logged or stringified, the value is replaced with ``.\n- Prevents accidental exposure of secrets in logs or error messages.\n\n## Anti-Pattern\n\nPassing sensitive data as plain strings, which can be accidentally logged, serialized, or leaked in error messages." - }, - { - "id": "data-duration", - "title": "Representing Time Spans with Duration", - "description": "Use Duration to model and manipulate time spans, enabling safe and expressive time-based logic.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Duration } from \"effect\";\n\n// Create durations using helpers\nconst oneSecond = Duration.seconds(1);\nconst fiveMinutes = Duration.minutes(5);\nconst twoHours = Duration.hours(2);\n\n// Add, subtract, and compare durations\nconst total = Duration.sum(oneSecond, fiveMinutes); // 5 min 1 sec\nconst isLonger = Duration.greaterThan(twoHours, fiveMinutes); // true\n\n// Convert to milliseconds or ISO string\nconst ms = Duration.toMillis(fiveMinutes); // 300000\nconst iso = Duration.formatIso(oneSecond); // \"PT1S\"\n```\n\n**Explanation:**\n\n- `Duration` is immutable and type-safe.\n- Use helpers for common intervals and arithmetic for composition.\n- Prefer `Duration` over raw numbers for all time-based logic.", - "antiPattern": "Using raw numbers (e.g., `5000` for 5 seconds) for time intervals, which is error-prone, hard to read, and less maintainable.", - "explanation": "Working with raw numbers for time intervals (e.g., milliseconds) is error-prone and hard to read. \n`Duration` provides a clear, expressive API for modeling time spans, improving code safety and maintainability.", - "content": "# Representing Time Spans with `Duration`\n\n## Guideline\n\nUse the `Duration` data type to represent and manipulate time intervals in a type-safe, human-readable, and composable way. \nThis enables robust time-based logic for scheduling, retries, timeouts, and more.\n\n## Rationale\n\nWorking with raw numbers for time intervals (e.g., milliseconds) is error-prone and hard to read. \n`Duration` provides a clear, expressive API for modeling time spans, improving code safety and maintainability.\n\n## Good Example\n\n```typescript\nimport { Duration } from \"effect\";\n\n// Create durations using helpers\nconst oneSecond = Duration.seconds(1);\nconst fiveMinutes = Duration.minutes(5);\nconst twoHours = Duration.hours(2);\n\n// Add, subtract, and compare durations\nconst total = Duration.sum(oneSecond, fiveMinutes); // 5 min 1 sec\nconst isLonger = Duration.greaterThan(twoHours, fiveMinutes); // true\n\n// Convert to milliseconds or ISO string\nconst ms = Duration.toMillis(fiveMinutes); // 300000\nconst iso = Duration.formatIso(oneSecond); // \"PT1S\"\n```\n\n**Explanation:**\n\n- `Duration` is immutable and type-safe.\n- Use helpers for common intervals and arithmetic for composition.\n- Prefer `Duration` over raw numbers for all time-based logic.\n\n## Anti-Pattern\n\nUsing raw numbers (e.g., `5000` for 5 seconds) for time intervals, which is error-prone, hard to read, and less maintainable." - }, - { - "id": "representing-time-spans-with-duration", - "title": "Representing Time Spans with Duration", - "description": "Use the Duration data type to represent time intervals instead of raw numbers.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "This example shows how to create and use `Duration` to make time-based operations clear and unambiguous.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\n\n// Create durations with clear, explicit units\nconst fiveSeconds = Duration.seconds(5);\nconst oneHundredMillis = Duration.millis(100);\n\n// Use them in Effect operators\nconst program = Effect.log(\"Starting...\").pipe(\n Effect.delay(oneHundredMillis),\n Effect.flatMap(() => Effect.log(\"Running after 100ms\")),\n Effect.timeout(fiveSeconds) // This whole operation must complete within 5 seconds\n);\n\n// Durations can also be compared\nconst isLonger = Duration.greaterThan(fiveSeconds, oneHundredMillis); // true\n\n// Demonstrate the duration functionality\nconst demonstration = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Duration Demonstration ===\");\n\n // Show duration values\n yield* Effect.logInfo(`Five seconds: ${Duration.toMillis(fiveSeconds)}ms`);\n yield* Effect.logInfo(\n `One hundred millis: ${Duration.toMillis(oneHundredMillis)}ms`\n );\n\n // Show comparison\n yield* Effect.logInfo(`Is 5 seconds longer than 100ms? ${isLonger}`);\n\n // Run the timed program\n yield* Effect.logInfo(\"Running timed program...\");\n yield* program;\n\n // Show more duration operations\n const combined = Duration.sum(fiveSeconds, oneHundredMillis);\n yield* Effect.logInfo(`Combined duration: ${Duration.toMillis(combined)}ms`);\n\n // Show different duration units\n const oneMinute = Duration.minutes(1);\n yield* Effect.logInfo(`One minute: ${Duration.toMillis(oneMinute)}ms`);\n\n const isMinuteLonger = Duration.greaterThan(oneMinute, fiveSeconds);\n yield* Effect.logInfo(`Is 1 minute longer than 5 seconds? ${isMinuteLonger}`);\n});\n\nEffect.runPromise(demonstration);\n```\n\n---", - "antiPattern": "Using raw numbers for time-based operations. This is ambiguous and error-prone.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: What does '2000' mean? Milliseconds? Seconds?\nconst program = Effect.log(\"Waiting...\").pipe(Effect.delay(2000));\n\n// This is especially dangerous when different parts of an application\n// use different conventions (e.g., one service uses seconds, another uses milliseconds).\n// Using Duration eliminates this entire class of bugs.\n```", - "explanation": "Using raw numbers to represent time is a common source of bugs and confusion. When you see `setTimeout(fn, 5000)`, it's not immediately clear if the unit is seconds or milliseconds without prior knowledge of the API.\n\n`Duration` solves this by making the unit explicit in the code. It provides a type-safe, immutable, and human-readable way to work with time intervals. This eliminates ambiguity and makes your code easier to read and maintain. Durations are used throughout Effect's time-based operators, such as `Effect.sleep`, `Effect.timeout`, and `Schedule`.\n\n---", - "content": "## Guideline\n\nWhen you need to represent a span of time (e.g., for a delay, timeout, or schedule), use the `Duration` data type. Create durations with expressive constructors like `Duration.seconds(5)`, `Duration.minutes(10)`, or `Duration.millis(500)`.\n\n---\n\n## Rationale\n\nUsing raw numbers to represent time is a common source of bugs and confusion. When you see `setTimeout(fn, 5000)`, it's not immediately clear if the unit is seconds or milliseconds without prior knowledge of the API.\n\n`Duration` solves this by making the unit explicit in the code. It provides a type-safe, immutable, and human-readable way to work with time intervals. This eliminates ambiguity and makes your code easier to read and maintain. Durations are used throughout Effect's time-based operators, such as `Effect.sleep`, `Effect.timeout`, and `Schedule`.\n\n---\n\n## Good Example\n\nThis example shows how to create and use `Duration` to make time-based operations clear and unambiguous.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\n\n// Create durations with clear, explicit units\nconst fiveSeconds = Duration.seconds(5);\nconst oneHundredMillis = Duration.millis(100);\n\n// Use them in Effect operators\nconst program = Effect.log(\"Starting...\").pipe(\n Effect.delay(oneHundredMillis),\n Effect.flatMap(() => Effect.log(\"Running after 100ms\")),\n Effect.timeout(fiveSeconds) // This whole operation must complete within 5 seconds\n);\n\n// Durations can also be compared\nconst isLonger = Duration.greaterThan(fiveSeconds, oneHundredMillis); // true\n\n// Demonstrate the duration functionality\nconst demonstration = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Duration Demonstration ===\");\n\n // Show duration values\n yield* Effect.logInfo(`Five seconds: ${Duration.toMillis(fiveSeconds)}ms`);\n yield* Effect.logInfo(\n `One hundred millis: ${Duration.toMillis(oneHundredMillis)}ms`\n );\n\n // Show comparison\n yield* Effect.logInfo(`Is 5 seconds longer than 100ms? ${isLonger}`);\n\n // Run the timed program\n yield* Effect.logInfo(\"Running timed program...\");\n yield* program;\n\n // Show more duration operations\n const combined = Duration.sum(fiveSeconds, oneHundredMillis);\n yield* Effect.logInfo(`Combined duration: ${Duration.toMillis(combined)}ms`);\n\n // Show different duration units\n const oneMinute = Duration.minutes(1);\n yield* Effect.logInfo(`One minute: ${Duration.toMillis(oneMinute)}ms`);\n\n const isMinuteLonger = Duration.greaterThan(oneMinute, fiveSeconds);\n yield* Effect.logInfo(`Is 1 minute longer than 5 seconds? ${isMinuteLonger}`);\n});\n\nEffect.runPromise(demonstration);\n```\n\n---\n\n## Anti-Pattern\n\nUsing raw numbers for time-based operations. This is ambiguous and error-prone.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: What does '2000' mean? Milliseconds? Seconds?\nconst program = Effect.log(\"Waiting...\").pipe(Effect.delay(2000));\n\n// This is especially dangerous when different parts of an application\n// use different conventions (e.g., one service uses seconds, another uses milliseconds).\n// Using Duration eliminates this entire class of bugs.\n```" - }, - { - "id": "getting-started-retry-on-failure", - "title": "Retry a Failed Operation with Effect.retry", - "description": "Retry failed operations with Effect.retry.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "```typescript\nimport { Effect, Schedule, pipe } from \"effect\";\n\nclass ApiError {\n readonly _tag = \"ApiError\";\n constructor(readonly status: number) {}\n}\n\nconst fetchUserData = (userId: string) =>\n Effect.tryPromise({\n try: async () => {\n const response = await fetch(`/api/users/${userId}`);\n if (!response.ok) throw new ApiError(response.status);\n return response.json();\n },\n catch: (error) => error as ApiError,\n });\n\n// Retry up to 3 times with 500ms between attempts\nconst fetchWithRetry = (userId: string) =>\n pipe(\n fetchUserData(userId),\n Effect.retry(\n Schedule.recurs(3).pipe(Schedule.addDelay(() => \"500 millis\"))\n ),\n Effect.catchAll((error) =>\n Effect.succeed({ error: `Failed after retries: ${error._tag}` })\n )\n );\n```", - "antiPattern": "", - "explanation": "Network requests fail. Databases time out. Services go down temporarily.\nInstead of failing immediately, you often want to retry a few times.\nEffect makes this a one-liner.", - "content": "# Retry a Failed Operation with Effect.retry\n\n## Guideline\n\nUse `Effect.retry` to automatically retry an Effect that fails. Combine it\nwith a `Schedule` to control how many times to retry and how long to wait\nbetween attempts.\n\n## Rationale\n\nNetwork requests fail. Databases time out. Services go down temporarily.\nInstead of failing immediately, you often want to retry a few times.\nEffect makes this a one-liner.\n\n## Simple Retry\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\n// An operation that might fail\nconst fetchData = Effect.tryPromise({\n try: () => fetch(\"https://api.example.com/data\"),\n catch: () => new Error(\"Network error\"),\n});\n\n// Retry up to 3 times\nconst withRetry = Effect.retry(fetchData, Schedule.recurs(3));\n```\n\n## Retry with Delay\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\nconst unreliableOperation = Effect.gen(function* () {\n const random = Math.random();\n if (random < 0.7) {\n return yield* Effect.fail(\"Random failure\");\n }\n return \"Success!\";\n});\n\n// Retry up to 3 times, waiting 1 second between attempts\nconst withDelay = Effect.retry(\n unreliableOperation,\n Schedule.recurs(3).pipe(Schedule.addDelay(() => \"1 second\"))\n);\n```\n\n## Common Retry Patterns\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\nconst operation = Effect.fail(\"temporary error\");\n\n// Retry 5 times immediately\nconst quick = Effect.retry(operation, Schedule.recurs(5));\n\n// Retry 3 times with 1 second between\nconst spaced = Effect.retry(\n operation, \n Schedule.spaced(\"1 second\").pipe(Schedule.intersect(Schedule.recurs(3)))\n);\n\n// Retry with exponential backoff (1s, 2s, 4s, 8s...)\nconst exponential = Effect.retry(\n operation,\n Schedule.exponential(\"1 second\").pipe(Schedule.intersect(Schedule.recurs(5)))\n);\n```\n\n## Good Example: Retrying an API Call\n\n```typescript\nimport { Effect, Schedule, pipe } from \"effect\";\n\nclass ApiError {\n readonly _tag = \"ApiError\";\n constructor(readonly status: number) {}\n}\n\nconst fetchUserData = (userId: string) =>\n Effect.tryPromise({\n try: async () => {\n const response = await fetch(`/api/users/${userId}`);\n if (!response.ok) throw new ApiError(response.status);\n return response.json();\n },\n catch: (error) => error as ApiError,\n });\n\n// Retry up to 3 times with 500ms between attempts\nconst fetchWithRetry = (userId: string) =>\n pipe(\n fetchUserData(userId),\n Effect.retry(\n Schedule.recurs(3).pipe(Schedule.addDelay(() => \"500 millis\"))\n ),\n Effect.catchAll((error) =>\n Effect.succeed({ error: `Failed after retries: ${error._tag}` })\n )\n );\n```\n\n## Quick Reference\n\n| Schedule | Behavior |\n|----------|----------|\n| `Schedule.recurs(3)` | Retry 3 times immediately |\n| `Schedule.spaced(\"1 second\")` | Retry forever with 1s delay |\n| `Schedule.exponential(\"100 millis\")` | Exponential backoff |\n| `Schedule.forever` | Retry indefinitely |\n\n## Key Points\n\n1. **Effect.retry** takes an Effect and a Schedule\n2. **Schedule controls** the retry timing and count\n3. **Combine schedules** with `.pipe()` for complex behavior\n4. **Still need error handling** - retries might all fail\n\n## What's Next?\n\n- Learn exponential backoff for production systems\n- Learn Schedule combinators for complex retry logic\n- Learn about circuit breakers for failing services" - }, - { - "id": "scheduling-retry-basics", - "title": "Retry Failed Operations", - "description": "Use Effect.retry with a Schedule to handle transient failures gracefully.", - "skillLevel": "beginner", - "useCases": [ - "scheduling" - ], - "example": "```typescript\nimport { Effect, Schedule, Data } from \"effect\"\n\n// ============================================\n// 1. Define error types\n// ============================================\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly message: string\n}> {}\n\nclass RateLimitError extends Data.TaggedError(\"RateLimitError\")<{\n readonly retryAfter: number\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly resource: string\n}> {}\n\n// ============================================\n// 2. Simulate a flaky API call\n// ============================================\n\nlet callCount = 0\nconst fetchData = Effect.gen(function* () {\n callCount++\n yield* Effect.log(`API call attempt ${callCount}`)\n\n // Simulate intermittent failures\n if (callCount < 3) {\n return yield* Effect.fail(new NetworkError({ message: \"Connection timeout\" }))\n }\n\n return { data: \"Success!\", attempts: callCount }\n})\n\n// ============================================\n// 3. Basic retry - fixed attempts\n// ============================================\n\nconst withBasicRetry = fetchData.pipe(\n Effect.retry(Schedule.recurs(5)) // Retry up to 5 times\n)\n\n// ============================================\n// 4. Retry with delay\n// ============================================\n\nconst withDelayedRetry = fetchData.pipe(\n Effect.retry(\n Schedule.spaced(\"500 millis\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n )\n )\n)\n\n// ============================================\n// 5. Retry only specific errors\n// ============================================\n\nconst fetchWithErrors = (shouldFail: boolean) =>\n Effect.gen(function* () {\n if (shouldFail) {\n // Randomly fail with different errors\n const random = Math.random()\n if (random < 0.5) {\n return yield* Effect.fail(new NetworkError({ message: \"Timeout\" }))\n } else if (random < 0.8) {\n return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 }))\n } else {\n return yield* Effect.fail(new NotFoundError({ resource: \"user:123\" }))\n }\n }\n return \"Data fetched!\"\n })\n\n// Only retry network and rate limit errors, not NotFoundError\nconst retryTransientOnly = fetchWithErrors(true).pipe(\n Effect.retry({\n schedule: Schedule.recurs(3),\n while: (error) =>\n error._tag === \"NetworkError\" || error._tag === \"RateLimitError\",\n })\n)\n\n// ============================================\n// 6. Retry with exponential backoff\n// ============================================\n\nconst withExponentialBackoff = fetchData.pipe(\n Effect.retry(\n Schedule.exponential(\"100 millis\", 2).pipe( // 100ms, 200ms, 400ms...\n Schedule.intersect(Schedule.recurs(5)) // Max 5 retries\n )\n )\n)\n\n// ============================================\n// 7. Run and observe\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting retry demo...\")\n \n // Reset counter\n callCount = 0\n \n const result = yield* withBasicRetry\n yield* Effect.log(`Final result: ${JSON.stringify(result)}`)\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Many failures are temporary:\n\n1. **Network issues** - Connection drops, timeouts\n2. **Rate limits** - Too many requests\n3. **Resource contention** - Database locks\n4. **Service restarts** - Brief unavailability\n\nAutomatic retries handle these without manual intervention.\n\n---", - "content": "## Guideline\n\nUse `Effect.retry` to automatically retry operations that fail due to transient errors like network timeouts.\n\n---\n\n## Rationale\n\nMany failures are temporary:\n\n1. **Network issues** - Connection drops, timeouts\n2. **Rate limits** - Too many requests\n3. **Resource contention** - Database locks\n4. **Service restarts** - Brief unavailability\n\nAutomatic retries handle these without manual intervention.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Schedule, Data } from \"effect\"\n\n// ============================================\n// 1. Define error types\n// ============================================\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly message: string\n}> {}\n\nclass RateLimitError extends Data.TaggedError(\"RateLimitError\")<{\n readonly retryAfter: number\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly resource: string\n}> {}\n\n// ============================================\n// 2. Simulate a flaky API call\n// ============================================\n\nlet callCount = 0\nconst fetchData = Effect.gen(function* () {\n callCount++\n yield* Effect.log(`API call attempt ${callCount}`)\n\n // Simulate intermittent failures\n if (callCount < 3) {\n return yield* Effect.fail(new NetworkError({ message: \"Connection timeout\" }))\n }\n\n return { data: \"Success!\", attempts: callCount }\n})\n\n// ============================================\n// 3. Basic retry - fixed attempts\n// ============================================\n\nconst withBasicRetry = fetchData.pipe(\n Effect.retry(Schedule.recurs(5)) // Retry up to 5 times\n)\n\n// ============================================\n// 4. Retry with delay\n// ============================================\n\nconst withDelayedRetry = fetchData.pipe(\n Effect.retry(\n Schedule.spaced(\"500 millis\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n )\n )\n)\n\n// ============================================\n// 5. Retry only specific errors\n// ============================================\n\nconst fetchWithErrors = (shouldFail: boolean) =>\n Effect.gen(function* () {\n if (shouldFail) {\n // Randomly fail with different errors\n const random = Math.random()\n if (random < 0.5) {\n return yield* Effect.fail(new NetworkError({ message: \"Timeout\" }))\n } else if (random < 0.8) {\n return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 }))\n } else {\n return yield* Effect.fail(new NotFoundError({ resource: \"user:123\" }))\n }\n }\n return \"Data fetched!\"\n })\n\n// Only retry network and rate limit errors, not NotFoundError\nconst retryTransientOnly = fetchWithErrors(true).pipe(\n Effect.retry({\n schedule: Schedule.recurs(3),\n while: (error) =>\n error._tag === \"NetworkError\" || error._tag === \"RateLimitError\",\n })\n)\n\n// ============================================\n// 6. Retry with exponential backoff\n// ============================================\n\nconst withExponentialBackoff = fetchData.pipe(\n Effect.retry(\n Schedule.exponential(\"100 millis\", 2).pipe( // 100ms, 200ms, 400ms...\n Schedule.intersect(Schedule.recurs(5)) // Max 5 retries\n )\n )\n)\n\n// ============================================\n// 7. Run and observe\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting retry demo...\")\n \n // Reset counter\n callCount = 0\n \n const result = yield* withBasicRetry\n yield* Effect.log(`Final result: ${JSON.stringify(result)}`)\n})\n\nEffect.runPromise(program)\n```\n\n## Retry Strategies\n\n| Strategy | When to Use |\n|----------|-------------|\n| `Schedule.recurs(n)` | Known transient failures |\n| `Schedule.spaced(d)` | Give service time to recover |\n| `Schedule.exponential(d)` | Rate limits, backpressure |\n| `Schedule.jittered(s)` | Avoid thundering herd |\n\n## Key Options\n\n```typescript\nEffect.retry({\n schedule: Schedule.recurs(3), // How to schedule retries\n while: (error) => isTransient(error), // Which errors to retry\n until: (error) => isFatal(error), // When to stop retrying\n})\n```\n\n## Best Practices\n\n1. **Don't retry everything** - Some errors are permanent\n2. **Add delays** - Give the system time to recover\n3. **Use backoff** - Avoid overwhelming a struggling service\n4. **Set limits** - Don't retry forever\n5. **Log attempts** - Know what's happening" - }, - { - "id": "http-retries", - "title": "Retry HTTP Requests with Backoff", - "description": "Use Schedule to retry failed HTTP requests with configurable backoff strategies.", - "skillLevel": "intermediate", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Schedule, Duration, Data } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from \"@effect/platform\"\n\n// ============================================\n// 1. Basic retry with exponential backoff\n// ============================================\n\nconst fetchWithRetry = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((response) => HttpClientResponse.json(response)),\n Effect.retry(\n Schedule.exponential(\"100 millis\", 2).pipe(\n Schedule.intersect(Schedule.recurs(5)), // Max 5 retries\n Schedule.jittered // Add randomness\n )\n )\n )\n })\n\n// ============================================\n// 2. Retry only specific status codes\n// ============================================\n\nclass RetryableHttpError extends Data.TaggedError(\"RetryableHttpError\")<{\n readonly status: number\n readonly message: string\n}> {}\n\nclass NonRetryableHttpError extends Data.TaggedError(\"NonRetryableHttpError\")<{\n readonly status: number\n readonly message: string\n}> {}\n\nconst isRetryable = (status: number): boolean =>\n status === 429 || // Rate limited\n status === 503 || // Service unavailable\n status === 502 || // Bad gateway\n status === 504 || // Gateway timeout\n status >= 500 // Server errors\n\nconst fetchWithSelectiveRetry = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status >= 400) {\n if (isRetryable(response.status)) {\n return Effect.fail(new RetryableHttpError({\n status: response.status,\n message: `HTTP ${response.status}`,\n }))\n }\n return Effect.fail(new NonRetryableHttpError({\n status: response.status,\n message: `HTTP ${response.status}`,\n }))\n }\n return Effect.succeed(response)\n }),\n Effect.retry({\n schedule: Schedule.exponential(\"200 millis\").pipe(\n Schedule.intersect(Schedule.recurs(3))\n ),\n while: (error) => error._tag === \"RetryableHttpError\",\n })\n )\n\n return yield* HttpClientResponse.json(response)\n })\n\n// ============================================\n// 3. Retry with logging\n// ============================================\n\nconst fetchWithRetryLogging = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.tapOutput((_, output) =>\n Effect.log(`Retry attempt, waiting ${Duration.toMillis(output)}ms`)\n )\n )\n ),\n Effect.tapError((error) => Effect.log(`Request failed: ${error}`))\n )\n })\n\n// ============================================\n// 4. Custom retry policy\n// ============================================\n\nconst customRetryPolicy = Schedule.exponential(\"500 millis\", 2).pipe(\n Schedule.intersect(Schedule.recurs(5)),\n Schedule.union(Schedule.spaced(\"30 seconds\")), // Also retry after 30s\n Schedule.whileOutput((duration) => Duration.lessThanOrEqualTo(duration, \"2 minutes\")),\n Schedule.jittered\n)\n\n// ============================================\n// 5. Retry respecting Retry-After header\n// ============================================\n\nconst fetchWithRetryAfter = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const makeRequest = client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status === 429) {\n const retryAfter = response.headers[\"retry-after\"]\n const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000\n\n return Effect.fail({\n _tag: \"RateLimited\" as const,\n delay,\n })\n }\n return Effect.succeed(response)\n })\n )\n\n return yield* makeRequest.pipe(\n Effect.retry(\n Schedule.recurWhile<{ _tag: \"RateLimited\"; delay: number }>(\n (error) => error._tag === \"RateLimited\"\n ).pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.delayed((_, error) => Duration.millis(error.delay))\n )\n ),\n Effect.flatMap((r) => HttpClientResponse.json(r))\n )\n })\n\n// ============================================\n// 6. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Fetching with retry...\")\n\n const data = yield* fetchWithRetry(\"https://api.example.com/data\").pipe(\n Effect.catchAll((error) => {\n return Effect.succeed({ error: \"All retries exhausted\" })\n })\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(data)}`)\n})\n```", - "antiPattern": "", - "explanation": "HTTP requests fail for transient reasons:\n\n1. **Network issues** - Temporary connectivity problems\n2. **Server overload** - 503 Service Unavailable\n3. **Rate limits** - 429 Too Many Requests\n4. **Timeouts** - Slow responses\n\nProper retry logic handles these gracefully.\n\n---", - "content": "## Guideline\n\nUse Effect's `retry` with `Schedule` to automatically retry failed HTTP requests with exponential backoff and jitter.\n\n---\n\n## Rationale\n\nHTTP requests fail for transient reasons:\n\n1. **Network issues** - Temporary connectivity problems\n2. **Server overload** - 503 Service Unavailable\n3. **Rate limits** - 429 Too Many Requests\n4. **Timeouts** - Slow responses\n\nProper retry logic handles these gracefully.\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Schedule, Duration, Data } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from \"@effect/platform\"\n\n// ============================================\n// 1. Basic retry with exponential backoff\n// ============================================\n\nconst fetchWithRetry = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((response) => HttpClientResponse.json(response)),\n Effect.retry(\n Schedule.exponential(\"100 millis\", 2).pipe(\n Schedule.intersect(Schedule.recurs(5)), // Max 5 retries\n Schedule.jittered // Add randomness\n )\n )\n )\n })\n\n// ============================================\n// 2. Retry only specific status codes\n// ============================================\n\nclass RetryableHttpError extends Data.TaggedError(\"RetryableHttpError\")<{\n readonly status: number\n readonly message: string\n}> {}\n\nclass NonRetryableHttpError extends Data.TaggedError(\"NonRetryableHttpError\")<{\n readonly status: number\n readonly message: string\n}> {}\n\nconst isRetryable = (status: number): boolean =>\n status === 429 || // Rate limited\n status === 503 || // Service unavailable\n status === 502 || // Bad gateway\n status === 504 || // Gateway timeout\n status >= 500 // Server errors\n\nconst fetchWithSelectiveRetry = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const response = yield* client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status >= 400) {\n if (isRetryable(response.status)) {\n return Effect.fail(new RetryableHttpError({\n status: response.status,\n message: `HTTP ${response.status}`,\n }))\n }\n return Effect.fail(new NonRetryableHttpError({\n status: response.status,\n message: `HTTP ${response.status}`,\n }))\n }\n return Effect.succeed(response)\n }),\n Effect.retry({\n schedule: Schedule.exponential(\"200 millis\").pipe(\n Schedule.intersect(Schedule.recurs(3))\n ),\n while: (error) => error._tag === \"RetryableHttpError\",\n })\n )\n\n return yield* HttpClientResponse.json(response)\n })\n\n// ============================================\n// 3. Retry with logging\n// ============================================\n\nconst fetchWithRetryLogging = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return yield* client.get(url).pipe(\n Effect.flatMap((r) => HttpClientResponse.json(r)),\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.tapOutput((_, output) =>\n Effect.log(`Retry attempt, waiting ${Duration.toMillis(output)}ms`)\n )\n )\n ),\n Effect.tapError((error) => Effect.log(`Request failed: ${error}`))\n )\n })\n\n// ============================================\n// 4. Custom retry policy\n// ============================================\n\nconst customRetryPolicy = Schedule.exponential(\"500 millis\", 2).pipe(\n Schedule.intersect(Schedule.recurs(5)),\n Schedule.union(Schedule.spaced(\"30 seconds\")), // Also retry after 30s\n Schedule.whileOutput((duration) => Duration.lessThanOrEqualTo(duration, \"2 minutes\")),\n Schedule.jittered\n)\n\n// ============================================\n// 5. Retry respecting Retry-After header\n// ============================================\n\nconst fetchWithRetryAfter = (url: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n const makeRequest = client.get(url).pipe(\n Effect.flatMap((response) => {\n if (response.status === 429) {\n const retryAfter = response.headers[\"retry-after\"]\n const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000\n\n return Effect.fail({\n _tag: \"RateLimited\" as const,\n delay,\n })\n }\n return Effect.succeed(response)\n })\n )\n\n return yield* makeRequest.pipe(\n Effect.retry(\n Schedule.recurWhile<{ _tag: \"RateLimited\"; delay: number }>(\n (error) => error._tag === \"RateLimited\"\n ).pipe(\n Schedule.intersect(Schedule.recurs(3)),\n Schedule.delayed((_, error) => Duration.millis(error.delay))\n )\n ),\n Effect.flatMap((r) => HttpClientResponse.json(r))\n )\n })\n\n// ============================================\n// 6. Usage\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Fetching with retry...\")\n\n const data = yield* fetchWithRetry(\"https://api.example.com/data\").pipe(\n Effect.catchAll((error) => {\n return Effect.succeed({ error: \"All retries exhausted\" })\n })\n )\n\n yield* Effect.log(`Result: ${JSON.stringify(data)}`)\n})\n```\n\n## Retry Schedules\n\n| Schedule | Behavior |\n|----------|----------|\n| `exponential(\"100ms\")` | 100ms, 200ms, 400ms... |\n| `fibonacci(\"100ms\")` | 100ms, 100ms, 200ms, 300ms... |\n| `spaced(\"1s\")` | 1s, 1s, 1s... (fixed) |\n| `jittered` | Add randomness |\n\n## Best Practices\n\n1. **Don't retry 4xx** - Client errors won't fix themselves\n2. **Use jitter** - Prevent thundering herd\n3. **Set max retries** - Don't retry forever\n4. **Log retries** - Know when they happen\n5. **Respect Retry-After** - Server knows best" - }, - { - "id": "retry-based-on-specific-errors", - "title": "Retry Operations Based on Specific Errors", - "description": "Use predicate-based retry policies to retry an operation only for specific, recoverable errors.", - "skillLevel": "intermediate", - "useCases": [ - "error-management" - ], - "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`.\n\n```typescript\nimport { Data, Effect, Schedule } from \"effect\";\n\n// Define specific, tagged errors for our API client\nclass ServerBusyError extends Data.TaggedError(\"ServerBusyError\") {}\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\") {}\n\nlet attemptCount = 0;\n\n// A flaky API call that can fail in different ways\nconst flakyApiCall = Effect.try({\n try: () => {\n attemptCount++;\n const random = Math.random();\n\n if (attemptCount <= 2) {\n // First two attempts fail with ServerBusyError (retryable)\n console.log(\n `Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`\n );\n throw new ServerBusyError();\n }\n\n // Third attempt succeeds\n console.log(`Attempt ${attemptCount}: API call succeeded!`);\n return { data: \"success\", attempt: attemptCount };\n },\n catch: (e) => e as ServerBusyError | NotFoundError,\n});\n\n// A predicate that returns true only for the error we want to retry\nconst isRetryableError = (e: ServerBusyError | NotFoundError) =>\n e._tag === \"ServerBusyError\";\n\n// A policy that retries 3 times, but only if the error is retryable\nconst selectiveRetryPolicy = Schedule.recurs(3).pipe(\n Schedule.whileInput(isRetryableError),\n Schedule.addDelay(() => \"100 millis\")\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Retry Based on Specific Errors Demo ===\");\n\n try {\n const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));\n yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);\n return result;\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n return null;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(\"Failed with NotFoundError - not retrying\");\n } else if (error instanceof ServerBusyError) {\n yield* Effect.logInfo(\"Failed with ServerBusyError after all retries\");\n } else {\n yield* Effect.logInfo(`Failed with unexpected error: ${error}`);\n }\n return null;\n })\n )\n);\n\n// Also demonstrate a case where NotFoundError is not retried\nconst demonstrateNotFound = Effect.gen(function* () {\n yield* Effect.logInfo(\"\\n=== Demonstrating Non-Retryable Error ===\");\n\n const alwaysNotFound = Effect.fail(new NotFoundError());\n\n const result = yield* alwaysNotFound.pipe(\n Effect.retry(selectiveRetryPolicy),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);\n return null;\n })\n )\n );\n\n return result;\n});\n\nEffect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));\n```\n\n---", - "antiPattern": "Using a generic `Effect.retry` that retries on all errors. This can lead to wasted resources and obscure permanent issues.\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\nimport { flakyApiCall } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This policy will retry even if the API returns a 404 Not Found.\n// This wastes time and network requests on an error that will never succeed.\nconst blindRetryPolicy = Schedule.recurs(3);\n\nconst program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy));\n```", - "explanation": "Not all errors are created equal. Retrying on a permanent error like \"permission denied\" or \"not found\" is pointless and can hide underlying issues. You only want to retry on _transient_, recoverable errors, such as network timeouts or \"server busy\" responses.\n\nBy adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs.\n\n---", - "content": "## Guideline\n\nTo selectively retry an operation, use `Effect.retry` with a `Schedule` that includes a predicate. The most common way is to use `Schedule.whileInput((error) => ...)`, which will continue retrying only as long as the predicate returns `true` for the error that occurred.\n\n---\n\n## Rationale\n\nNot all errors are created equal. Retrying on a permanent error like \"permission denied\" or \"not found\" is pointless and can hide underlying issues. You only want to retry on _transient_, recoverable errors, such as network timeouts or \"server busy\" responses.\n\nBy adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs.\n\n---\n\n## Good Example\n\nThis 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`.\n\n```typescript\nimport { Data, Effect, Schedule } from \"effect\";\n\n// Define specific, tagged errors for our API client\nclass ServerBusyError extends Data.TaggedError(\"ServerBusyError\") {}\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\") {}\n\nlet attemptCount = 0;\n\n// A flaky API call that can fail in different ways\nconst flakyApiCall = Effect.try({\n try: () => {\n attemptCount++;\n const random = Math.random();\n\n if (attemptCount <= 2) {\n // First two attempts fail with ServerBusyError (retryable)\n console.log(\n `Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`\n );\n throw new ServerBusyError();\n }\n\n // Third attempt succeeds\n console.log(`Attempt ${attemptCount}: API call succeeded!`);\n return { data: \"success\", attempt: attemptCount };\n },\n catch: (e) => e as ServerBusyError | NotFoundError,\n});\n\n// A predicate that returns true only for the error we want to retry\nconst isRetryableError = (e: ServerBusyError | NotFoundError) =>\n e._tag === \"ServerBusyError\";\n\n// A policy that retries 3 times, but only if the error is retryable\nconst selectiveRetryPolicy = Schedule.recurs(3).pipe(\n Schedule.whileInput(isRetryableError),\n Schedule.addDelay(() => \"100 millis\")\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Retry Based on Specific Errors Demo ===\");\n\n try {\n const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));\n yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);\n return result;\n } catch (error) {\n yield* Effect.logInfo(\"This won't be reached due to Effect error handling\");\n return null;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(\"Failed with NotFoundError - not retrying\");\n } else if (error instanceof ServerBusyError) {\n yield* Effect.logInfo(\"Failed with ServerBusyError after all retries\");\n } else {\n yield* Effect.logInfo(`Failed with unexpected error: ${error}`);\n }\n return null;\n })\n )\n);\n\n// Also demonstrate a case where NotFoundError is not retried\nconst demonstrateNotFound = Effect.gen(function* () {\n yield* Effect.logInfo(\"\\n=== Demonstrating Non-Retryable Error ===\");\n\n const alwaysNotFound = Effect.fail(new NotFoundError());\n\n const result = yield* alwaysNotFound.pipe(\n Effect.retry(selectiveRetryPolicy),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);\n return null;\n })\n )\n );\n\n return result;\n});\n\nEffect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));\n```\n\n---\n\n## Anti-Pattern\n\nUsing a generic `Effect.retry` that retries on all errors. This can lead to wasted resources and obscure permanent issues.\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\nimport { flakyApiCall } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This policy will retry even if the API returns a 404 Not Found.\n// This wastes time and network requests on an error that will never succeed.\nconst blindRetryPolicy = Schedule.recurs(3);\n\nconst program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy));\n```" - }, - { - "id": "stream-run-for-effects", - "title": "Run a Pipeline for its Side Effects", - "description": "Use Stream.runDrain to execute a stream for its side effects when you don't need the final values.", - "skillLevel": "beginner", - "useCases": [ - "building-data-pipelines" - ], - "example": "This example creates a stream of tasks. For each task, it performs a side effect (logging it as \"complete\"). `Stream.runDrain` executes the pipeline, ensuring all logs are written, but without collecting the `void` results of each logging operation.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\nconst tasks = [\"task 1\", \"task 2\", \"task 3\"];\n\n// A function that performs a side effect for a task\nconst completeTask = (task: string): Effect.Effect =>\n Effect.log(`Completing ${task}`);\n\nconst program = Stream.fromIterable(tasks).pipe(\n // For each task, run the side-effectful operation\n Stream.mapEffect(completeTask, { concurrency: 1 }),\n // Run the stream for its effects, discarding the `void` results\n Stream.runDrain\n);\n\nconst programWithLogging = Effect.gen(function* () {\n yield* program;\n yield* Effect.log(\"\\nAll tasks have been processed.\");\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n... level=INFO msg=\"Completing task 1\"\n... level=INFO msg=\"Completing task 2\"\n... level=INFO msg=\"Completing task 3\"\n\nAll tasks have been processed.\n*/\n```", - "antiPattern": "The anti-pattern is using `Stream.runCollect` when you only care about the side effects. This needlessly consumes memory and can lead to crashes.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same tasks and completeTask function ...\n\nconst program = Stream.fromIterable(tasks).pipe(\n Stream.mapEffect(completeTask, { concurrency: 1 }),\n // Anti-pattern: Collecting results that we are just going to ignore\n Stream.runCollect\n);\n\nEffect.runPromise(program).then((results) => {\n // The `results` variable here is a Chunk of `[void, void, void]`.\n // It served no purpose but consumed memory.\n console.log(\n `\\nAll tasks processed. Unnecessarily collected ${results.length} empty results.`\n );\n});\n```\n\nWhile this works for a small array of three items, it's a dangerous habit. If the `tasks` array contained millions of items, this code would create a `Chunk` with millions of `void` values, consuming a significant amount of memory for no reason and potentially crashing the application. `Stream.runDrain` avoids this problem entirely.", - "explanation": "Not all pipelines are designed to produce a final list of values. Often, the goal is to perform an action for each item—write it to a database, send it to a message queue, or log it to a file. In these \"fire and forget\" scenarios, collecting the results is not just unnecessary; it's a performance anti-pattern.\n\n`Stream.runDrain` is the perfect tool for this job:\n\n1. **Memory Efficiency**: This is its primary advantage. `runDrain` processes each item and then immediately discards it, resulting in constant, minimal memory usage. This makes it the only safe choice for processing extremely large or infinite streams.\n2. **Clarity of Intent**: Using `runDrain` clearly communicates that you are interested in the successful execution of the stream's effects, not in its output values. The final `Effect` it produces resolves to `void`, reinforcing that no value is returned.\n3. **Performance**: By avoiding the overhead of allocating and managing a growing list in memory, `runDrain` can be faster for pipelines with a very large number of small items.\n\n---", - "content": "## Guideline\n\nTo run a stream purely for its side effects without accumulating the results in memory, use the `Stream.runDrain` sink.\n\n---\n\n## Rationale\n\nNot all pipelines are designed to produce a final list of values. Often, the goal is to perform an action for each item—write it to a database, send it to a message queue, or log it to a file. In these \"fire and forget\" scenarios, collecting the results is not just unnecessary; it's a performance anti-pattern.\n\n`Stream.runDrain` is the perfect tool for this job:\n\n1. **Memory Efficiency**: This is its primary advantage. `runDrain` processes each item and then immediately discards it, resulting in constant, minimal memory usage. This makes it the only safe choice for processing extremely large or infinite streams.\n2. **Clarity of Intent**: Using `runDrain` clearly communicates that you are interested in the successful execution of the stream's effects, not in its output values. The final `Effect` it produces resolves to `void`, reinforcing that no value is returned.\n3. **Performance**: By avoiding the overhead of allocating and managing a growing list in memory, `runDrain` can be faster for pipelines with a very large number of small items.\n\n---\n\n## Good Example\n\nThis example creates a stream of tasks. For each task, it performs a side effect (logging it as \"complete\"). `Stream.runDrain` executes the pipeline, ensuring all logs are written, but without collecting the `void` results of each logging operation.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n\nconst tasks = [\"task 1\", \"task 2\", \"task 3\"];\n\n// A function that performs a side effect for a task\nconst completeTask = (task: string): Effect.Effect =>\n Effect.log(`Completing ${task}`);\n\nconst program = Stream.fromIterable(tasks).pipe(\n // For each task, run the side-effectful operation\n Stream.mapEffect(completeTask, { concurrency: 1 }),\n // Run the stream for its effects, discarding the `void` results\n Stream.runDrain\n);\n\nconst programWithLogging = Effect.gen(function* () {\n yield* program;\n yield* Effect.log(\"\\nAll tasks have been processed.\");\n});\n\nEffect.runPromise(programWithLogging);\n/*\nOutput:\n... level=INFO msg=\"Completing task 1\"\n... level=INFO msg=\"Completing task 2\"\n... level=INFO msg=\"Completing task 3\"\n\nAll tasks have been processed.\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is using `Stream.runCollect` when you only care about the side effects. This needlessly consumes memory and can lead to crashes.\n\n```typescript\nimport { Effect, Stream } from \"effect\";\n// ... same tasks and completeTask function ...\n\nconst program = Stream.fromIterable(tasks).pipe(\n Stream.mapEffect(completeTask, { concurrency: 1 }),\n // Anti-pattern: Collecting results that we are just going to ignore\n Stream.runCollect\n);\n\nEffect.runPromise(program).then((results) => {\n // The `results` variable here is a Chunk of `[void, void, void]`.\n // It served no purpose but consumed memory.\n console.log(\n `\\nAll tasks processed. Unnecessarily collected ${results.length} empty results.`\n );\n});\n```\n\nWhile this works for a small array of three items, it's a dangerous habit. If the `tasks` array contained millions of items, this code would create a `Chunk` with millions of `void` values, consuming a significant amount of memory for no reason and potentially crashing the application. `Stream.runDrain` avoids this problem entirely." - }, - { - "id": "run-background-tasks-with-fork", - "title": "Run Background Tasks with Effect.fork", - "description": "Use Effect.fork to start a non-blocking background process and manage its lifecycle via its Fiber.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This program forks a background process that logs a \"tick\" every second. The main process does its own work for 5 seconds and then explicitly interrupts the background logger before exiting.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\n// A long-running effect that logs a message every second, forever\n// Effect.forever creates an infinite loop that repeats the effect\n// This simulates a background service like a health check or monitoring task\nconst tickingClock = Effect.log(\"tick\").pipe(\n Effect.delay(\"1 second\"), // Wait 1 second between ticks\n Effect.forever // Repeat indefinitely - this creates an infinite effect\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Forking the ticking clock into the background.\");\n\n // Start the clock, but don't wait for it.\n // Effect.fork creates a new fiber that runs concurrently with the main program\n // The main fiber continues immediately without waiting for the background task\n // This is essential for non-blocking background operations\n const clockFiber = yield* Effect.fork(tickingClock);\n\n // At this point, we have two fibers running:\n // 1. The main fiber (this program)\n // 2. The background clock fiber (ticking every second)\n\n yield* Effect.log(\"Main process is now doing other work for 5 seconds...\");\n\n // Simulate the main application doing work\n // While this sleep happens, the background clock continues ticking\n // This demonstrates true concurrency - both fibers run simultaneously\n yield* Effect.sleep(\"5 seconds\");\n\n yield* Effect.log(\"Main process is done. Interrupting the clock fiber.\");\n\n // Stop the background process.\n // Fiber.interrupt sends an interruption signal to the fiber\n // This allows the fiber to perform cleanup operations before terminating\n // Without this, the background task would continue running indefinitely\n yield* Fiber.interrupt(clockFiber);\n\n // Important: Always clean up background fibers to prevent resource leaks\n // In a real application, you might want to:\n // 1. Use Fiber.join instead of interrupt to wait for graceful completion\n // 2. Handle interruption signals within the background task\n // 3. Implement proper shutdown procedures\n\n yield* Effect.log(\"Program finished.\");\n\n // Key concepts demonstrated:\n // 1. Fork creates concurrent fibers without blocking\n // 2. Background tasks run independently of the main program\n // 3. Fiber interruption provides controlled shutdown\n // 4. Multiple fibers can run simultaneously on the same thread pool\n});\n\n// This example shows how to:\n// - Run background tasks that don't block the main program\n// - Manage fiber lifecycles (create, run, interrupt)\n// - Coordinate between multiple concurrent operations\n// - Properly clean up resources when shutting down\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "The anti-pattern is using `Effect.fork` when you immediately need the result of the computation. This is an overly complicated and less readable way of just running the effect directly.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst someEffect = Effect.succeed(42);\n\n// ❌ WRONG: This is unnecessarily complex.\nconst program = Effect.gen(function* () {\n const fiber = yield* Effect.fork(someEffect);\n // You immediately wait for the result, defeating the purpose of forking.\n const result = yield* Fiber.join(fiber);\n return result;\n});\n\n// ✅ CORRECT: Just run the effect directly if you need its result right away.\nconst simplerProgram = Effect.gen(function* () {\n const result = yield* someEffect;\n return result;\n});\n```", - "explanation": "Unlike `Effect.all` or a direct `yield*`, which wait for the computation to complete, `Effect.fork` is a \"fire and forget\" operation. It starts the effect on a new, concurrent fiber and immediately returns control to the parent fiber.\n\nThis is essential for managing long-running background tasks like:\n\n- A web server listener.\n- A message queue consumer.\n- A periodic cache cleanup job.\n\nThe returned `Fiber` object is your remote control for the background task. You can use `Fiber.interrupt` to safely stop it (ensuring all its finalizers are run) or `Fiber.join` to wait for it to complete at some later point.\n\n---", - "content": "## Guideline\n\nTo start an `Effect` in the background without blocking the current execution flow, use `Effect.fork`. This immediately returns a `Fiber`, which is a handle to the running computation that you can use to manage its lifecycle (e.g., interrupt it or wait for its result).\n\n---\n\n## Rationale\n\nUnlike `Effect.all` or a direct `yield*`, which wait for the computation to complete, `Effect.fork` is a \"fire and forget\" operation. It starts the effect on a new, concurrent fiber and immediately returns control to the parent fiber.\n\nThis is essential for managing long-running background tasks like:\n\n- A web server listener.\n- A message queue consumer.\n- A periodic cache cleanup job.\n\nThe returned `Fiber` object is your remote control for the background task. You can use `Fiber.interrupt` to safely stop it (ensuring all its finalizers are run) or `Fiber.join` to wait for it to complete at some later point.\n\n---\n\n## Good Example\n\nThis program forks a background process that logs a \"tick\" every second. The main process does its own work for 5 seconds and then explicitly interrupts the background logger before exiting.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\n// A long-running effect that logs a message every second, forever\n// Effect.forever creates an infinite loop that repeats the effect\n// This simulates a background service like a health check or monitoring task\nconst tickingClock = Effect.log(\"tick\").pipe(\n Effect.delay(\"1 second\"), // Wait 1 second between ticks\n Effect.forever // Repeat indefinitely - this creates an infinite effect\n);\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Forking the ticking clock into the background.\");\n\n // Start the clock, but don't wait for it.\n // Effect.fork creates a new fiber that runs concurrently with the main program\n // The main fiber continues immediately without waiting for the background task\n // This is essential for non-blocking background operations\n const clockFiber = yield* Effect.fork(tickingClock);\n\n // At this point, we have two fibers running:\n // 1. The main fiber (this program)\n // 2. The background clock fiber (ticking every second)\n\n yield* Effect.log(\"Main process is now doing other work for 5 seconds...\");\n\n // Simulate the main application doing work\n // While this sleep happens, the background clock continues ticking\n // This demonstrates true concurrency - both fibers run simultaneously\n yield* Effect.sleep(\"5 seconds\");\n\n yield* Effect.log(\"Main process is done. Interrupting the clock fiber.\");\n\n // Stop the background process.\n // Fiber.interrupt sends an interruption signal to the fiber\n // This allows the fiber to perform cleanup operations before terminating\n // Without this, the background task would continue running indefinitely\n yield* Fiber.interrupt(clockFiber);\n\n // Important: Always clean up background fibers to prevent resource leaks\n // In a real application, you might want to:\n // 1. Use Fiber.join instead of interrupt to wait for graceful completion\n // 2. Handle interruption signals within the background task\n // 3. Implement proper shutdown procedures\n\n yield* Effect.log(\"Program finished.\");\n\n // Key concepts demonstrated:\n // 1. Fork creates concurrent fibers without blocking\n // 2. Background tasks run independently of the main program\n // 3. Fiber interruption provides controlled shutdown\n // 4. Multiple fibers can run simultaneously on the same thread pool\n});\n\n// This example shows how to:\n// - Run background tasks that don't block the main program\n// - Manage fiber lifecycles (create, run, interrupt)\n// - Coordinate between multiple concurrent operations\n// - Properly clean up resources when shutting down\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nThe anti-pattern is using `Effect.fork` when you immediately need the result of the computation. This is an overly complicated and less readable way of just running the effect directly.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst someEffect = Effect.succeed(42);\n\n// ❌ WRONG: This is unnecessarily complex.\nconst program = Effect.gen(function* () {\n const fiber = yield* Effect.fork(someEffect);\n // You immediately wait for the result, defeating the purpose of forking.\n const result = yield* Fiber.join(fiber);\n return result;\n});\n\n// ✅ CORRECT: Just run the effect directly if you need its result right away.\nconst simplerProgram = Effect.gen(function* () {\n const result = yield* someEffect;\n return result;\n});\n```" - }, - { - "id": "run-effects-in-parallel-with-all", - "title": "Run Independent Effects in Parallel with Effect.all", - "description": "Use Effect.all to execute a collection of independent effects concurrently.", - "skillLevel": "intermediate", - "useCases": [ - "concurrency" - ], - "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.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Simulate fetching a user, takes 1 second\nconst fetchUser = Effect.succeed({ id: 1, name: \"Paul\" }).pipe(\n Effect.delay(\"1 second\")\n);\n\n// Simulate fetching posts, takes 1.5 seconds\nconst fetchPosts = Effect.succeed([{ title: \"Effect is great\" }]).pipe(\n Effect.delay(\"1.5 seconds\")\n);\n\n// Run both effects concurrently - must specify concurrency option!\nconst program = Effect.all([fetchUser, fetchPosts], {\n concurrency: \"unbounded\",\n});\n\n// The resulting effect will succeed with a tuple: [{id, name}, [{title}]]\n// Total execution time will be ~1.5 seconds (the duration of the longest task).\nconst programWithLogging = Effect.gen(function* () {\n const results = yield* program;\n yield* Effect.log(`Results: ${JSON.stringify(results)}`);\n return results;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "The anti-pattern is running independent tasks sequentially using `Effect.gen`. This is inefficient and unnecessarily slows down your application.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { fetchUser, fetchPosts } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This is inefficient.\nconst program = Effect.gen(function* () {\n // fetchUser runs and completes...\n const user = yield* fetchUser;\n // ...only then does fetchPosts begin.\n const posts = yield* fetchPosts;\n return [user, posts];\n});\n\n// Total execution time will be ~2.5 seconds (1s + 1.5s),\n// which is a full second slower than the parallel version.\nEffect.runPromise(program).then(console.log);\n```", - "explanation": "Running tasks sequentially when they could be done in parallel is a common source of performance bottlenecks. `Effect.all` is the solution. It's the direct equivalent of `Promise.all` in the Effect ecosystem.\n\nInstead of waiting for Task A to finish before starting Task B, `Effect.all` starts all tasks simultaneously. The total time to complete is determined by the duration of the _longest_ running effect, not the sum of all durations. If any single effect in the collection fails, the entire `Effect.all` will fail immediately.\n\n---", - "content": "## Guideline\n\nWhen you have multiple `Effect`s that do not depend on each other's results, run them concurrently using `Effect.all`. This will execute all effects at the same time and return a new `Effect` that succeeds with a tuple containing all the results.\n\n---\n\n## Rationale\n\nRunning tasks sequentially when they could be done in parallel is a common source of performance bottlenecks. `Effect.all` is the solution. It's the direct equivalent of `Promise.all` in the Effect ecosystem.\n\nInstead of waiting for Task A to finish before starting Task B, `Effect.all` starts all tasks simultaneously. The total time to complete is determined by the duration of the _longest_ running effect, not the sum of all durations. If any single effect in the collection fails, the entire `Effect.all` will fail immediately.\n\n---\n\n## Good Example\n\nImagine 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.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Simulate fetching a user, takes 1 second\nconst fetchUser = Effect.succeed({ id: 1, name: \"Paul\" }).pipe(\n Effect.delay(\"1 second\")\n);\n\n// Simulate fetching posts, takes 1.5 seconds\nconst fetchPosts = Effect.succeed([{ title: \"Effect is great\" }]).pipe(\n Effect.delay(\"1.5 seconds\")\n);\n\n// Run both effects concurrently - must specify concurrency option!\nconst program = Effect.all([fetchUser, fetchPosts], {\n concurrency: \"unbounded\",\n});\n\n// The resulting effect will succeed with a tuple: [{id, name}, [{title}]]\n// Total execution time will be ~1.5 seconds (the duration of the longest task).\nconst programWithLogging = Effect.gen(function* () {\n const results = yield* program;\n yield* Effect.log(`Results: ${JSON.stringify(results)}`);\n return results;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nThe anti-pattern is running independent tasks sequentially using `Effect.gen`. This is inefficient and unnecessarily slows down your application.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { fetchUser, fetchPosts } from \"./somewhere\"; // From previous example\n\n// ❌ WRONG: This is inefficient.\nconst program = Effect.gen(function* () {\n // fetchUser runs and completes...\n const user = yield* fetchUser;\n // ...only then does fetchPosts begin.\n const posts = yield* fetchPosts;\n return [user, posts];\n});\n\n// Total execution time will be ~2.5 seconds (1s + 1.5s),\n// which is a full second slower than the parallel version.\nEffect.runPromise(program).then(console.log);\n```" - }, - { - "id": "getting-started-run-in-parallel", - "title": "Run Multiple Effects in Parallel with Effect.all", - "description": "Run multiple Effects in parallel with Effect.all.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "```typescript\nimport { Effect, pipe } from \"effect\";\n\n// Simulate fetching data from different sources\nconst fetchUser = Effect.succeed({ id: 1, name: \"Alice\" }).pipe(\n Effect.delay(\"100 millis\")\n);\n\nconst fetchPosts = Effect.succeed([\n { id: 1, title: \"Hello World\" },\n { id: 2, title: \"Effect is awesome\" },\n]).pipe(Effect.delay(\"150 millis\"));\n\nconst fetchSettings = Effect.succeed({ theme: \"dark\" }).pipe(\n Effect.delay(\"50 millis\")\n);\n\n// Fetch all data in parallel\nconst program = Effect.gen(function* () {\n const [user, posts, settings] = yield* Effect.all(\n [fetchUser, fetchPosts, fetchSettings],\n { concurrency: \"unbounded\" }\n );\n\n yield* Effect.log(`Loaded ${user.name} with ${posts.length} posts`);\n return { user, posts, settings };\n});\n\nEffect.runPromise(program);\n```", - "antiPattern": "", - "explanation": "Real applications often need to do multiple things at once - fetch data from\nseveral APIs, process multiple files, etc. `Effect.all` lets you express\nthis naturally without callback hell or complex Promise.all patterns.", - "content": "# Run Multiple Effects in Parallel with Effect.all\n\n## Guideline\n\nUse `Effect.all` to run multiple Effects concurrently and wait for all of\nthem to complete. By default, Effects run sequentially - add the\n`concurrency` option to run them in parallel.\n\n## Rationale\n\nReal applications often need to do multiple things at once - fetch data from\nseveral APIs, process multiple files, etc. `Effect.all` lets you express\nthis naturally without callback hell or complex Promise.all patterns.\n\n## Sequential vs Parallel\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst task1 = Effect.delay(Effect.succeed(\"Task 1 done\"), \"1 second\");\nconst task2 = Effect.delay(Effect.succeed(\"Task 2 done\"), \"1 second\");\nconst task3 = Effect.delay(Effect.succeed(\"Task 3 done\"), \"1 second\");\n\n// SEQUENTIAL (default) - takes 3 seconds\nconst sequential = Effect.all([task1, task2, task3]);\n\n// PARALLEL - takes 1 second (all run at once)\nconst parallel = Effect.all([task1, task2, task3], { concurrency: \"unbounded\" });\n```\n\n## Good Example\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\n// Simulate fetching data from different sources\nconst fetchUser = Effect.succeed({ id: 1, name: \"Alice\" }).pipe(\n Effect.delay(\"100 millis\")\n);\n\nconst fetchPosts = Effect.succeed([\n { id: 1, title: \"Hello World\" },\n { id: 2, title: \"Effect is awesome\" },\n]).pipe(Effect.delay(\"150 millis\"));\n\nconst fetchSettings = Effect.succeed({ theme: \"dark\" }).pipe(\n Effect.delay(\"50 millis\")\n);\n\n// Fetch all data in parallel\nconst program = Effect.gen(function* () {\n const [user, posts, settings] = yield* Effect.all(\n [fetchUser, fetchPosts, fetchSettings],\n { concurrency: \"unbounded\" }\n );\n\n yield* Effect.log(`Loaded ${user.name} with ${posts.length} posts`);\n return { user, posts, settings };\n});\n\nEffect.runPromise(program);\n```\n\n## Concurrency Options\n\n| Option | Behavior |\n|--------|----------|\n| `{ concurrency: 1 }` | Sequential (default) |\n| `{ concurrency: 3 }` | Max 3 at a time |\n| `{ concurrency: \"unbounded\" }` | All at once |\n| `{ concurrency: \"inherit\" }` | Use parent's setting |\n\n## Using with Objects\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Effect.all also works with objects - keys are preserved\nconst result = Effect.all({\n user: fetchUser,\n posts: fetchPosts,\n settings: fetchSettings,\n}, { concurrency: \"unbounded\" });\n\n// result type: Effect<{ user: User, posts: Post[], settings: Settings }>\n```\n\n## Error Handling\n\nIf any Effect fails, `Effect.all` fails with that error (by default):\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst willFail = Effect.fail(\"Oops!\");\nconst willSucceed = Effect.succeed(\"OK\");\n\nconst program = Effect.all([willSucceed, willFail, willSucceed]);\n// This fails with \"Oops!\" - other results are discarded\n```\n\n## Key Points\n\n1. **Default is sequential** - add `concurrency` option for parallelism\n2. **All results collected** - returns array or object matching input shape\n3. **Fail-fast by default** - first failure stops everything\n4. **Type-safe** - TypeScript knows the exact result type\n\n## What's Next?\n\n- Learn `Effect.race` to take the first result\n- Learn `Effect.forEach` for processing collections\n- Learn about Fibers for more control over concurrent tasks" - }, - { - "id": "stream-running-collecting", - "title": "Running and Collecting Stream Results", - "description": "Choose the right Stream.run* method based on what you need from the results.", - "skillLevel": "beginner", - "useCases": [ - "streams-getting-started" - ], - "example": "```typescript\nimport { Effect, Stream, Option } from \"effect\"\n\nconst numbers = Stream.make(1, 2, 3, 4, 5)\n\n// ============================================\n// runCollect - Get all results as a Chunk\n// ============================================\n\nconst collectAll = numbers.pipe(\n Stream.map((n) => n * 10),\n Stream.runCollect\n)\n\nEffect.runPromise(collectAll).then((chunk) => {\n console.log([...chunk]) // [10, 20, 30, 40, 50]\n})\n\n// ============================================\n// runForEach - Process each item\n// ============================================\n\nconst processEach = numbers.pipe(\n Stream.runForEach((n) =>\n Effect.log(`Processing: ${n}`)\n )\n)\n\nEffect.runPromise(processEach)\n// Logs: Processing: 1, Processing: 2, etc.\n\n// ============================================\n// runDrain - Run for side effects only\n// ============================================\n\nconst withSideEffects = numbers.pipe(\n Stream.tap((n) => Effect.log(`Saw: ${n}`)),\n Stream.runDrain // Discard values, just run\n)\n\n// ============================================\n// runHead - Get first value only\n// ============================================\n\nconst getFirst = numbers.pipe(\n Stream.runHead\n)\n\nEffect.runPromise(getFirst).then((option) => {\n if (Option.isSome(option)) {\n console.log(`First: ${option.value}`) // First: 1\n }\n})\n\n// ============================================\n// runLast - Get last value only\n// ============================================\n\nconst getLast = numbers.pipe(\n Stream.runLast\n)\n\nEffect.runPromise(getLast).then((option) => {\n if (Option.isSome(option)) {\n console.log(`Last: ${option.value}`) // Last: 5\n }\n})\n\n// ============================================\n// runFold - Accumulate into single result\n// ============================================\n\nconst sum = numbers.pipe(\n Stream.runFold(0, (acc, n) => acc + n)\n)\n\nEffect.runPromise(sum).then((total) => {\n console.log(`Sum: ${total}`) // Sum: 15\n})\n\n// ============================================\n// runCount - Count elements\n// ============================================\n\nconst count = numbers.pipe(Stream.runCount)\n\nEffect.runPromise(count).then((n) => {\n console.log(`Count: ${n}`) // Count: 5\n})\n```", - "antiPattern": "", - "explanation": "Effect provides several ways to consume a stream, each optimized for different use cases:\n\n| Method | Returns | Use When |\n|--------|---------|----------|\n| **runCollect** | `Chunk` | Need all results in memory |\n| **runForEach** | `void` | Process each item for side effects |\n| **runDrain** | `void` | Run for side effects, ignore values |\n| **runHead** | `Option` | Only need first value |\n| **runLast** | `Option` | Only need last value |\n| **runFold** | `S` | Accumulate into single result |\n\n---", - "content": "## Guideline\n\nStreams are lazy - nothing happens until you run them. Choose your run method based on what you need: all results, per-item effects, or just completion.\n\n---\n\n## Rationale\n\nEffect provides several ways to consume a stream, each optimized for different use cases:\n\n| Method | Returns | Use When |\n|--------|---------|----------|\n| **runCollect** | `Chunk` | Need all results in memory |\n| **runForEach** | `void` | Process each item for side effects |\n| **runDrain** | `void` | Run for side effects, ignore values |\n| **runHead** | `Option` | Only need first value |\n| **runLast** | `Option` | Only need last value |\n| **runFold** | `S` | Accumulate into single result |\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option } from \"effect\"\n\nconst numbers = Stream.make(1, 2, 3, 4, 5)\n\n// ============================================\n// runCollect - Get all results as a Chunk\n// ============================================\n\nconst collectAll = numbers.pipe(\n Stream.map((n) => n * 10),\n Stream.runCollect\n)\n\nEffect.runPromise(collectAll).then((chunk) => {\n console.log([...chunk]) // [10, 20, 30, 40, 50]\n})\n\n// ============================================\n// runForEach - Process each item\n// ============================================\n\nconst processEach = numbers.pipe(\n Stream.runForEach((n) =>\n Effect.log(`Processing: ${n}`)\n )\n)\n\nEffect.runPromise(processEach)\n// Logs: Processing: 1, Processing: 2, etc.\n\n// ============================================\n// runDrain - Run for side effects only\n// ============================================\n\nconst withSideEffects = numbers.pipe(\n Stream.tap((n) => Effect.log(`Saw: ${n}`)),\n Stream.runDrain // Discard values, just run\n)\n\n// ============================================\n// runHead - Get first value only\n// ============================================\n\nconst getFirst = numbers.pipe(\n Stream.runHead\n)\n\nEffect.runPromise(getFirst).then((option) => {\n if (Option.isSome(option)) {\n console.log(`First: ${option.value}`) // First: 1\n }\n})\n\n// ============================================\n// runLast - Get last value only\n// ============================================\n\nconst getLast = numbers.pipe(\n Stream.runLast\n)\n\nEffect.runPromise(getLast).then((option) => {\n if (Option.isSome(option)) {\n console.log(`Last: ${option.value}`) // Last: 5\n }\n})\n\n// ============================================\n// runFold - Accumulate into single result\n// ============================================\n\nconst sum = numbers.pipe(\n Stream.runFold(0, (acc, n) => acc + n)\n)\n\nEffect.runPromise(sum).then((total) => {\n console.log(`Sum: ${total}`) // Sum: 15\n})\n\n// ============================================\n// runCount - Count elements\n// ============================================\n\nconst count = numbers.pipe(Stream.runCount)\n\nEffect.runPromise(count).then((n) => {\n console.log(`Count: ${n}`) // Count: 5\n})\n```\n\n## Choosing the Right Method\n\n```\nWhat do you need from the stream?\n├── All values → runCollect\n├── Process each with side effects → runForEach\n├── Just run it (ignore values) → runDrain\n├── Only first value → runHead\n├── Only last value → runLast\n├── Accumulate to one result → runFold\n└── Count elements → runCount\n```\n\n## Memory Considerations\n\n- **runCollect** loads ALL values into memory - use only for bounded streams\n- **runForEach** processes one at a time - memory efficient\n- **runFold** accumulates incrementally - memory efficient\n- **runHead** stops after first value - very efficient" - }, - { - "id": "safely-bracket-resource-usage", - "title": "Safely Bracket Resource Usage with `acquireRelease`", - "description": "Bracket the use of a resource between an `acquire` and a `release` effect.", - "skillLevel": "beginner", - "useCases": [ - "resource-management" - ], - "example": "```typescript\nimport { Effect, Console } from \"effect\";\n\n// A mock resource that needs to be managed\nconst getDbConnection = Effect.sync(() => ({ id: Math.random() })).pipe(\n Effect.tap(() => Effect.log(\"Connection Acquired\"))\n);\n\nconst closeDbConnection = (conn: {\n id: number;\n}): Effect.Effect =>\n Effect.log(`Connection ${conn.id} Released`);\n\n// The program that uses the resource\nconst program = Effect.acquireRelease(\n getDbConnection, // 1. acquire\n (connection) => closeDbConnection(connection) // 2. cleanup\n).pipe(\n Effect.tap((connection) =>\n Effect.log(`Using connection ${connection.id} to run query...`)\n )\n);\n\nEffect.runPromise(Effect.scoped(program));\n\n/*\nOutput:\nConnection Acquired\nUsing connection 0.12345... to run query...\nConnection 0.12345... Released\n*/\n```\n\n**Explanation:**\nBy using `Effect.acquireRelease`, the `closeDbConnection` logic is guaranteed to run after the main logic completes. This creates a self-contained, leak-proof unit of work that can be safely composed into larger programs.", - "antiPattern": "Using a standard `try...finally` block with `async/await`. While it handles success and failure cases, it is **not interruption-safe**. If the fiber executing the `Promise` is interrupted by Effect's structured concurrency, the `finally` block is not guaranteed to run, leading to resource leaks.\n\n```typescript\n// ANTI-PATTERN: Not interruption-safe\nasync function getUser() {\n const connection = await getDbConnectionPromise(); // acquire\n try {\n return await useConnectionPromise(connection); // use\n } finally {\n // This block may not run if the fiber is interrupted!\n await closeConnectionPromise(connection); // release\n }\n}\n```", - "explanation": "This pattern is the foundation of resource safety in Effect. It provides a composable and interruption-safe alternative to a standard `try...finally` block. The `release` effect is guaranteed to execute, preventing resource leaks which are common in complex asynchronous applications, especially those involving concurrency where tasks can be cancelled.", - "content": "# Safely Bracket Resource Usage with `acquireRelease`\n\n## Guideline\n\nWrap the acquisition, usage, and release of a resource within an `Effect.acquireRelease` call. This ensures the resource's cleanup logic is executed, regardless of whether the usage logic succeeds, fails, or is interrupted.\n\n## Rationale\n\nThis pattern is the foundation of resource safety in Effect. It provides a composable and interruption-safe alternative to a standard `try...finally` block. The `release` effect is guaranteed to execute, preventing resource leaks which are common in complex asynchronous applications, especially those involving concurrency where tasks can be cancelled.\n\n## Good Example\n\n```typescript\nimport { Effect, Console } from \"effect\";\n\n// A mock resource that needs to be managed\nconst getDbConnection = Effect.sync(() => ({ id: Math.random() })).pipe(\n Effect.tap(() => Effect.log(\"Connection Acquired\"))\n);\n\nconst closeDbConnection = (conn: {\n id: number;\n}): Effect.Effect =>\n Effect.log(`Connection ${conn.id} Released`);\n\n// The program that uses the resource\nconst program = Effect.acquireRelease(\n getDbConnection, // 1. acquire\n (connection) => closeDbConnection(connection) // 2. cleanup\n).pipe(\n Effect.tap((connection) =>\n Effect.log(`Using connection ${connection.id} to run query...`)\n )\n);\n\nEffect.runPromise(Effect.scoped(program));\n\n/*\nOutput:\nConnection Acquired\nUsing connection 0.12345... to run query...\nConnection 0.12345... Released\n*/\n```\n\n**Explanation:**\nBy using `Effect.acquireRelease`, the `closeDbConnection` logic is guaranteed to run after the main logic completes. This creates a self-contained, leak-proof unit of work that can be safely composed into larger programs.\n\n## Anti-Pattern\n\nUsing a standard `try...finally` block with `async/await`. While it handles success and failure cases, it is **not interruption-safe**. If the fiber executing the `Promise` is interrupted by Effect's structured concurrency, the `finally` block is not guaranteed to run, leading to resource leaks.\n\n```typescript\n// ANTI-PATTERN: Not interruption-safe\nasync function getUser() {\n const connection = await getDbConnectionPromise(); // acquire\n try {\n return await useConnectionPromise(connection); // use\n } finally {\n // This block may not run if the fiber is interrupted!\n await closeConnectionPromise(connection); // release\n }\n}\n```" - }, - { - "id": "scheduling-pattern-repeat-effect-on-fixed-interval", - "title": "Scheduling Pattern 1: Repeat an Effect on a Fixed Interval", - "description": "Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks.", - "skillLevel": "intermediate", - "useCases": [ - "scheduling" - ], - "example": "This example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\ninterface ServiceStatus {\n readonly service: string;\n readonly url: string;\n readonly isHealthy: boolean;\n readonly responseTime: number;\n readonly lastChecked: number;\n}\n\n// Mock health check that calls an endpoint\nconst checkServiceHealth = (\n url: string,\n service: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n\n // Simulate HTTP call with occasional failures\n const isHealthy = Math.random() > 0.1; // 90% success rate\n const responseTime = Math.random() * 500; // 0-500ms\n\n yield* Effect.sleep(Duration.millis(Math.round(responseTime)));\n\n if (!isHealthy) {\n yield* Effect.fail(new Error(`${service} is unhealthy`));\n }\n\n return {\n service,\n url,\n isHealthy: true,\n responseTime: Math.round(Date.now() - startTime),\n lastChecked: Date.now(),\n };\n });\n\n// Health check for multiple services\ninterface HealthCheckConfig {\n readonly services: Array<{\n readonly name: string;\n readonly url: string;\n }>;\n readonly intervalSeconds: number;\n}\n\n// Keep track of service status\nconst serviceStatuses = new Map();\n\n// Check all services and report status\nconst checkAllServices = (\n config: HealthCheckConfig\n): Effect.Effect =>\n Effect.gen(function* () {\n for (const service of config.services) {\n const status = yield* checkServiceHealth(service.url, service.name).pipe(\n Effect.either\n );\n\n if (status._tag === \"Right\") {\n serviceStatuses.set(service.name, status.right);\n console.log(\n `✓ ${service.name}: OK (${status.right.responseTime}ms)`\n );\n } else {\n console.log(`✗ ${service.name}: FAILED`);\n // Keep last known status if available\n }\n }\n });\n\n// Create the repeating health check\nconst createHealthCheckScheduler = (\n config: HealthCheckConfig\n): Effect.Effect =>\n checkAllServices(config).pipe(\n // Schedule with fixed interval (fixed = ignore execution time)\n Effect.repeat(\n Schedule.fixed(Duration.seconds(config.intervalSeconds))\n )\n );\n\n// Report current status\nconst reportStatus = (): Effect.Effect =>\n Effect.sync(() => {\n if (serviceStatuses.size === 0) {\n console.log(\"\\n[STATUS] No services checked yet\");\n return;\n }\n\n console.log(\"\\n[STATUS REPORT]\");\n for (const [service, status] of serviceStatuses) {\n const ago = Math.round((Date.now() - status.lastChecked) / 1000);\n console.log(\n ` ${service}: ${status.isHealthy ? \"✓\" : \"✗\"} (checked ${ago}s ago)`\n );\n }\n });\n\n// Run health checker in background and check status periodically\nconst program = Effect.gen(function* () {\n const config: HealthCheckConfig = {\n services: [\n { name: \"API\", url: \"https://api.example.com/health\" },\n { name: \"Database\", url: \"https://db.example.com/health\" },\n { name: \"Cache\", url: \"https://cache.example.com/health\" },\n ],\n intervalSeconds: 5, // Check every 5 seconds\n };\n\n // Fork the health checker to run in background\n const checker = yield* createHealthCheckScheduler(config).pipe(\n Effect.fork\n );\n\n // Check and report status every 15 seconds for 60 seconds\n yield* reportStatus().pipe(\n Effect.repeat(\n Schedule.addDelay(\n Schedule.recurs(3), // 3 repetitions = 4 total (initial + 3)\n () => Duration.seconds(15)\n )\n )\n );\n\n // Interrupt the background checker\n yield* checker.interrupt();\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Defines service health checks** that may fail\n2. **Uses Schedule.fixed** to repeat every 5 seconds\n3. **Handles failures gracefully** (keeps last known status)\n4. **Runs in background** while main logic continues\n5. **Reports current status** at intervals\n\n---", - "antiPattern": "", - "explanation": "Many production systems need periodic operations:\n\n- **Health checks**: Poll service availability every 30 seconds\n- **Cache refresh**: Update cache every 5 minutes\n- **Metrics collection**: Gather system metrics every 10 seconds\n- **Data sync**: Sync data with remote service periodically\n- **Cleanup tasks**: Remove stale data nightly\n\nWithout proper scheduling:\n\n- Manual polling with `while` loops wastes CPU (busy-waiting)\n- Thread.sleep blocks threads, preventing other work\n- No automatic restart on failure\n- Difficult to test deterministically\n\nWith `Schedule.fixed`:\n\n- Efficient, non-blocking repetition\n- Automatic failure handling and retry\n- Testable with TestClock\n- Clean, declarative syntax\n\n---", - "content": "## Guideline\n\nWhen you need to run an effect repeatedly at regular intervals (e.g., every 5 seconds, every 30 minutes), use `Schedule.fixed` to specify the interval. This creates a schedule that repeats the effect indefinitely or until a condition stops it, with precise timing between executions.\n\n---\n\n## Rationale\n\nMany production systems need periodic operations:\n\n- **Health checks**: Poll service availability every 30 seconds\n- **Cache refresh**: Update cache every 5 minutes\n- **Metrics collection**: Gather system metrics every 10 seconds\n- **Data sync**: Sync data with remote service periodically\n- **Cleanup tasks**: Remove stale data nightly\n\nWithout proper scheduling:\n\n- Manual polling with `while` loops wastes CPU (busy-waiting)\n- Thread.sleep blocks threads, preventing other work\n- No automatic restart on failure\n- Difficult to test deterministically\n\nWith `Schedule.fixed`:\n\n- Efficient, non-blocking repetition\n- Automatic failure handling and retry\n- Testable with TestClock\n- Clean, declarative syntax\n\n---\n\n## Good Example\n\nThis example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status.\n\n```typescript\nimport { Effect, Schedule, Duration } from \"effect\";\n\ninterface ServiceStatus {\n readonly service: string;\n readonly url: string;\n readonly isHealthy: boolean;\n readonly responseTime: number;\n readonly lastChecked: number;\n}\n\n// Mock health check that calls an endpoint\nconst checkServiceHealth = (\n url: string,\n service: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const startTime = Date.now();\n\n // Simulate HTTP call with occasional failures\n const isHealthy = Math.random() > 0.1; // 90% success rate\n const responseTime = Math.random() * 500; // 0-500ms\n\n yield* Effect.sleep(Duration.millis(Math.round(responseTime)));\n\n if (!isHealthy) {\n yield* Effect.fail(new Error(`${service} is unhealthy`));\n }\n\n return {\n service,\n url,\n isHealthy: true,\n responseTime: Math.round(Date.now() - startTime),\n lastChecked: Date.now(),\n };\n });\n\n// Health check for multiple services\ninterface HealthCheckConfig {\n readonly services: Array<{\n readonly name: string;\n readonly url: string;\n }>;\n readonly intervalSeconds: number;\n}\n\n// Keep track of service status\nconst serviceStatuses = new Map();\n\n// Check all services and report status\nconst checkAllServices = (\n config: HealthCheckConfig\n): Effect.Effect =>\n Effect.gen(function* () {\n for (const service of config.services) {\n const status = yield* checkServiceHealth(service.url, service.name).pipe(\n Effect.either\n );\n\n if (status._tag === \"Right\") {\n serviceStatuses.set(service.name, status.right);\n console.log(\n `✓ ${service.name}: OK (${status.right.responseTime}ms)`\n );\n } else {\n console.log(`✗ ${service.name}: FAILED`);\n // Keep last known status if available\n }\n }\n });\n\n// Create the repeating health check\nconst createHealthCheckScheduler = (\n config: HealthCheckConfig\n): Effect.Effect =>\n checkAllServices(config).pipe(\n // Schedule with fixed interval (fixed = ignore execution time)\n Effect.repeat(\n Schedule.fixed(Duration.seconds(config.intervalSeconds))\n )\n );\n\n// Report current status\nconst reportStatus = (): Effect.Effect =>\n Effect.sync(() => {\n if (serviceStatuses.size === 0) {\n console.log(\"\\n[STATUS] No services checked yet\");\n return;\n }\n\n console.log(\"\\n[STATUS REPORT]\");\n for (const [service, status] of serviceStatuses) {\n const ago = Math.round((Date.now() - status.lastChecked) / 1000);\n console.log(\n ` ${service}: ${status.isHealthy ? \"✓\" : \"✗\"} (checked ${ago}s ago)`\n );\n }\n });\n\n// Run health checker in background and check status periodically\nconst program = Effect.gen(function* () {\n const config: HealthCheckConfig = {\n services: [\n { name: \"API\", url: \"https://api.example.com/health\" },\n { name: \"Database\", url: \"https://db.example.com/health\" },\n { name: \"Cache\", url: \"https://cache.example.com/health\" },\n ],\n intervalSeconds: 5, // Check every 5 seconds\n };\n\n // Fork the health checker to run in background\n const checker = yield* createHealthCheckScheduler(config).pipe(\n Effect.fork\n );\n\n // Check and report status every 15 seconds for 60 seconds\n yield* reportStatus().pipe(\n Effect.repeat(\n Schedule.addDelay(\n Schedule.recurs(3), // 3 repetitions = 4 total (initial + 3)\n () => Duration.seconds(15)\n )\n )\n );\n\n // Interrupt the background checker\n yield* checker.interrupt();\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Defines service health checks** that may fail\n2. **Uses Schedule.fixed** to repeat every 5 seconds\n3. **Handles failures gracefully** (keeps last known status)\n4. **Runs in background** while main logic continues\n5. **Reports current status** at intervals\n\n---\n\n## Advanced: Adaptive Interval Based on Conditions\n\nAdjust interval dynamically based on service health:\n\n```typescript\nconst createAdaptiveHealthCheckScheduler = (\n config: HealthCheckConfig\n): Effect.Effect =>\n Effect.gen(function* () {\n let failureCount = 0;\n let currentInterval = config.intervalSeconds;\n\n while (true) {\n const startTime = Date.now();\n\n // Run health check\n const allHealthy = yield* checkAllServices(config).pipe(\n Effect.map(() => true),\n Effect.catchAll(() => Effect.succeed(false))\n );\n\n if (allHealthy) {\n // All healthy, reset interval and failure count\n failureCount = 0;\n currentInterval = config.intervalSeconds;\n console.log(`[ADAPTIVE] All healthy, using normal interval (${currentInterval}s)`);\n } else {\n // Some unhealthy, increase check frequency\n failureCount++;\n currentInterval = Math.max(5, config.intervalSeconds / (failureCount + 1));\n console.log(\n `[ADAPTIVE] Unhealthy detected, increasing frequency (${currentInterval.toFixed(1)}s)`\n );\n }\n\n // Wait for the configured interval (minus execution time)\n const elapsedMs = Date.now() - startTime;\n const remainingMs = Math.max(0, currentInterval * 1000 - elapsedMs);\n\n if (remainingMs > 0) {\n yield* Effect.sleep(Duration.millis(remainingMs));\n }\n }\n });\n```\n\n---\n\n## Advanced: Rate-Limited Health Check with Backoff\n\nHandle rate limiting from endpoints:\n\n```typescript\ninterface RateLimitAwareConfig extends HealthCheckConfig {\n readonly maxChecksPerMinute: number;\n readonly backoffOnRateLimit: boolean;\n}\n\nconst createRateLimitAwareHealthCheck = (\n config: RateLimitAwareConfig\n): Effect.Effect =>\n Effect.gen(function* () {\n let checksInWindow = 0;\n let windowStart = Date.now();\n let backoffUntil = 0;\n\n while (true) {\n // Check if in backoff period\n if (Date.now() < backoffUntil) {\n const waitMs = backoffUntil - Date.now();\n console.log(`[RATE-LIMIT] Backing off for ${Math.round(waitMs)}ms`);\n yield* Effect.sleep(Duration.millis(waitMs));\n backoffUntil = 0;\n }\n\n // Reset window if needed\n if (Date.now() - windowStart > 60000) {\n checksInWindow = 0;\n windowStart = Date.now();\n }\n\n // Check if we can perform check\n if (checksInWindow >= config.maxChecksPerMinute) {\n const waitMs = 60000 - (Date.now() - windowStart);\n console.log(`[RATE-LIMIT] Rate limit reached, waiting ${waitMs}ms`);\n yield* Effect.sleep(Duration.millis(waitMs));\n checksInWindow = 0;\n windowStart = Date.now();\n }\n\n // Perform the health check\n yield* checkAllServices(config).pipe(\n Effect.tap(() => {\n checksInWindow++;\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error.message.includes(\"429\") || error.message.includes(\"rate limit\")) {\n if (config.backoffOnRateLimit) {\n backoffUntil = Date.now() + 60000; // Back off for 1 minute\n console.log(`[RATE-LIMIT] Hit 429, backing off`);\n }\n }\n return undefined;\n })\n )\n );\n\n // Wait for configured interval\n yield* Effect.sleep(Duration.seconds(config.intervalSeconds));\n }\n });\n```\n\n---\n\n## Advanced: Health Check with Exponential Backoff on Failure\n\nIncrease check frequency when services are unhealthy, then back off:\n\n```typescript\nconst createSmartHealthCheckScheduler = (\n config: HealthCheckConfig\n): Effect.Effect =>\n Effect.gen(function* () {\n const healthySchedule = Schedule.fixed(Duration.seconds(config.intervalSeconds));\n const unhealthySchedule = Schedule.fixed(Duration.seconds(10)); // Check every 10s when unhealthy\n const recoverySchedule = Schedule.fixed(Duration.seconds(30)); // Extended checks during recovery\n\n let state: \"healthy\" | \"unhealthy\" | \"recovering\" = \"healthy\";\n let unhealthyCount = 0;\n\n while (true) {\n const checkResult = yield* checkAllServices(config).pipe(\n Effect.either\n );\n\n if (checkResult._tag === \"Right\") {\n if (state === \"unhealthy\") {\n state = \"recovering\";\n unhealthyCount = 0;\n console.log(`[STATE] Transitioning to RECOVERING`);\n } else if (state === \"recovering\" && unhealthyCount > 3) {\n state = \"healthy\";\n console.log(`[STATE] Transitioning to HEALTHY`);\n } else if (state === \"recovering\") {\n unhealthyCount++;\n }\n } else {\n if (state !== \"unhealthy\") {\n state = \"unhealthy\";\n unhealthyCount = 0;\n console.log(`[STATE] Transitioning to UNHEALTHY`);\n }\n unhealthyCount++;\n }\n\n // Choose schedule based on state\n const nextDelay =\n state === \"healthy\"\n ? config.intervalSeconds\n : state === \"unhealthy\"\n ? 10\n : 30;\n\n yield* Effect.sleep(Duration.seconds(nextDelay));\n }\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use fixed-interval scheduling when:**\n\n- Polling external services (health checks, status updates)\n- Running background maintenance tasks\n- Collecting metrics at regular intervals\n- Syncing data periodically\n- Running cleanup jobs at specific times\n\n⚠️ **Trade-offs:**\n\n- Execution time affects precision (use `Schedule.addDelay` if needed)\n- No jitter by default (add manually if thundering herd is concern)\n- Runs indefinitely (need explicit interrupt or condition)\n- Imprecise for critical real-time work\n\n---\n\n## See Also\n\n- [Control Repetition with Schedule](./control-repetition-with-schedule.mdx) - Schedule fundamentals\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background execution\n- [Understand Fibers as Lightweight Threads](./understand-fibers-as-lightweight-threads.mdx) - Fiber concurrency\n- [Handle Flaky Operations with Retry & Timeout](./handle-flaky-operations-with-retry-timeout.mdx) - Error handling in polling" - }, - { - "id": "scheduling-pattern-exponential-backoff", - "title": "Scheduling Pattern 2: Implement Exponential Backoff for Retries", - "description": "Use exponential backoff with jitter for retries to prevent overwhelming failing services and improve success likelihood through smart timing.", - "skillLevel": "intermediate", - "useCases": [ - "error-handling-resilience" - ], - "example": "This example demonstrates exponential backoff with jitter for retrying a flaky API call.\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\ninterface RetryStats {\n readonly attempt: number;\n readonly delay: number;\n readonly lastError?: Error;\n}\n\n// Simulate flaky API that fails first 3 times, succeeds on 4th\nlet attemptCount = 0;\n\nconst flakyApiCall = (): Effect.Effect<{ status: string }> =>\n Effect.gen(function* () {\n attemptCount++;\n yield* Effect.log(`[API] Attempt ${attemptCount}`);\n\n if (attemptCount < 4) {\n yield* Effect.fail(new Error(\"Service temporarily unavailable (503)\"));\n }\n\n return { status: \"ok\" };\n });\n\n// Calculate exponential backoff with jitter\ninterface BackoffConfig {\n readonly baseDelayMs: number;\n readonly maxDelayMs: number;\n readonly maxRetries: number;\n}\n\nconst exponentialBackoffWithJitter = (config: BackoffConfig) => {\n let attempt = 0;\n\n // Calculate delay for this attempt\n const calculateDelay = (): number => {\n const exponential = config.baseDelayMs * Math.pow(2, attempt);\n const withJitter = exponential * (0.5 + Math.random() * 0.5); // ±50% jitter\n const capped = Math.min(withJitter, config.maxDelayMs);\n\n yield* Effect.log(\n `[BACKOFF] Attempt ${attempt + 1}: ${Math.round(capped)}ms delay`\n );\n\n return Math.round(capped);\n };\n\n return Effect.gen(function* () {\n const effect = flakyApiCall();\n\n let lastError: Error | undefined;\n\n for (attempt = 0; attempt < config.maxRetries; attempt++) {\n const result = yield* effect.pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attempt + 1}`);\n return result.right;\n }\n\n lastError = result.left;\n\n if (attempt < config.maxRetries - 1) {\n const delay = calculateDelay();\n yield* Effect.sleep(`${delay} millis`);\n }\n }\n\n yield* Effect.log(\n `[FAILURE] All ${config.maxRetries} attempts exhausted`\n );\n yield* Effect.fail(lastError);\n });\n};\n\n// Run with exponential backoff\nconst program = exponentialBackoffWithJitter({\n baseDelayMs: 100,\n maxDelayMs: 5000,\n maxRetries: 5,\n});\n\nconsole.log(\n `\\n[START] Retrying flaky API with exponential backoff\\n`\n);\n\nEffect.runPromise(program).then(\n (result) => console.log(`\\n[RESULT] ${JSON.stringify(result)}\\n`),\n (error) => console.error(`\\n[ERROR] ${error.message}\\n`)\n);\n```\n\nOutput demonstrates increasing delays with jitter:\n```\n[START] Retrying flaky API with exponential backoff\n\n[API] Attempt 1\n[BACKOFF] Attempt 1: 78ms delay\n[API] Attempt 2\n[BACKOFF] Attempt 2: 192ms delay\n[API] Attempt 3\n[BACKOFF] Attempt 3: 356ms delay\n[API] Attempt 4\n[SUCCESS] Succeeded on attempt 4\n\n[RESULT] {\"status\":\"ok\"}\n```\n\n---", - "antiPattern": "", - "explanation": "Naive retry strategies fail under load:\n\n**Immediate retry**:\n- All failures retry at once\n- Fails service under load (recovery takes longer)\n- Leads to cascade failure\n\n**Fixed backoff** (e.g., 1 second always):\n- No pressure reduction during recovery\n- Multiple clients cause thundering herd\n- Predictable = synchronized retries\n\n**Exponential backoff**:\n- Gives failing service time to recover\n- Each retry waits progressively longer\n- Without jitter, synchronized retries still hammer service\n\n**Exponential backoff + jitter**:\n- Spreads retry attempts over time\n- Failures de-correlate across clients\n- Service recovery time properly utilized\n- Success likelihood increases with each retry\n\nReal-world example: 100 clients fail simultaneously\n- **Immediate retry**: 100 requests in milliseconds → failure\n- **Fixed backoff**: 100 requests at exactly 1s → failure\n- **Exponential**: 100 requests at 100ms, 200ms, 400ms, 800ms → recovery → success\n\n---", - "content": "## Guideline\n\nWhen retrying failed operations, use exponential backoff with jitter: delay doubles on each retry (with random jitter), up to a maximum. This prevents:\n\n- **Thundering herd**: All clients retrying simultaneously\n- **Cascade failures**: Overwhelming a recovering service\n- **Resource exhaustion**: Too many queued retry attempts\n\nFormula: `delay = min(maxDelay, baseDelay * 2^attempt + random_jitter)`\n\n---\n\n## Rationale\n\nNaive retry strategies fail under load:\n\n**Immediate retry**:\n- All failures retry at once\n- Fails service under load (recovery takes longer)\n- Leads to cascade failure\n\n**Fixed backoff** (e.g., 1 second always):\n- No pressure reduction during recovery\n- Multiple clients cause thundering herd\n- Predictable = synchronized retries\n\n**Exponential backoff**:\n- Gives failing service time to recover\n- Each retry waits progressively longer\n- Without jitter, synchronized retries still hammer service\n\n**Exponential backoff + jitter**:\n- Spreads retry attempts over time\n- Failures de-correlate across clients\n- Service recovery time properly utilized\n- Success likelihood increases with each retry\n\nReal-world example: 100 clients fail simultaneously\n- **Immediate retry**: 100 requests in milliseconds → failure\n- **Fixed backoff**: 100 requests at exactly 1s → failure\n- **Exponential**: 100 requests at 100ms, 200ms, 400ms, 800ms → recovery → success\n\n---\n\n## Good Example\n\nThis example demonstrates exponential backoff with jitter for retrying a flaky API call.\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\ninterface RetryStats {\n readonly attempt: number;\n readonly delay: number;\n readonly lastError?: Error;\n}\n\n// Simulate flaky API that fails first 3 times, succeeds on 4th\nlet attemptCount = 0;\n\nconst flakyApiCall = (): Effect.Effect<{ status: string }> =>\n Effect.gen(function* () {\n attemptCount++;\n yield* Effect.log(`[API] Attempt ${attemptCount}`);\n\n if (attemptCount < 4) {\n yield* Effect.fail(new Error(\"Service temporarily unavailable (503)\"));\n }\n\n return { status: \"ok\" };\n });\n\n// Calculate exponential backoff with jitter\ninterface BackoffConfig {\n readonly baseDelayMs: number;\n readonly maxDelayMs: number;\n readonly maxRetries: number;\n}\n\nconst exponentialBackoffWithJitter = (config: BackoffConfig) => {\n let attempt = 0;\n\n // Calculate delay for this attempt\n const calculateDelay = (): number => {\n const exponential = config.baseDelayMs * Math.pow(2, attempt);\n const withJitter = exponential * (0.5 + Math.random() * 0.5); // ±50% jitter\n const capped = Math.min(withJitter, config.maxDelayMs);\n\n yield* Effect.log(\n `[BACKOFF] Attempt ${attempt + 1}: ${Math.round(capped)}ms delay`\n );\n\n return Math.round(capped);\n };\n\n return Effect.gen(function* () {\n const effect = flakyApiCall();\n\n let lastError: Error | undefined;\n\n for (attempt = 0; attempt < config.maxRetries; attempt++) {\n const result = yield* effect.pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attempt + 1}`);\n return result.right;\n }\n\n lastError = result.left;\n\n if (attempt < config.maxRetries - 1) {\n const delay = calculateDelay();\n yield* Effect.sleep(`${delay} millis`);\n }\n }\n\n yield* Effect.log(\n `[FAILURE] All ${config.maxRetries} attempts exhausted`\n );\n yield* Effect.fail(lastError);\n });\n};\n\n// Run with exponential backoff\nconst program = exponentialBackoffWithJitter({\n baseDelayMs: 100,\n maxDelayMs: 5000,\n maxRetries: 5,\n});\n\nconsole.log(\n `\\n[START] Retrying flaky API with exponential backoff\\n`\n);\n\nEffect.runPromise(program).then(\n (result) => console.log(`\\n[RESULT] ${JSON.stringify(result)}\\n`),\n (error) => console.error(`\\n[ERROR] ${error.message}\\n`)\n);\n```\n\nOutput demonstrates increasing delays with jitter:\n```\n[START] Retrying flaky API with exponential backoff\n\n[API] Attempt 1\n[BACKOFF] Attempt 1: 78ms delay\n[API] Attempt 2\n[BACKOFF] Attempt 2: 192ms delay\n[API] Attempt 3\n[BACKOFF] Attempt 3: 356ms delay\n[API] Attempt 4\n[SUCCESS] Succeeded on attempt 4\n\n[RESULT] {\"status\":\"ok\"}\n```\n\n---\n\n## Advanced: Effect Schedule with Exponential Backoff\n\nUse Effect's `Schedule` API for declarative exponential backoff:\n\n```typescript\nimport { Effect, Schedule } from \"effect\";\n\nconst exponentialBackoffSchedule = (baseDelayMs: number, maxDelayMs: number) =>\n Schedule.exponential(baseDelayMs).pipe(\n // Add jitter: ±20% randomization\n Schedule.jittered(0.2, 0.2),\n // Cap maximum delay\n Schedule.mapDelay((delay) => Math.min(delay, maxDelayMs)),\n // Log each retry attempt\n Schedule.tapInput((error) =>\n Effect.log(`[RETRY] Retrying after error: ${error.message}`)\n )\n );\n\n// Use in Effect.retry\nconst robustApiCall = flakyApiCall().pipe(\n Effect.retry(\n exponentialBackoffSchedule(100, 5000).pipe(\n // Max 5 retries\n Schedule.upTo(5)\n )\n ),\n Effect.tap(() => Effect.log(\"[SUCCESS] API call succeeded\"))\n);\n\nEffect.runPromise(robustApiCall);\n```\n\n---\n\n## Advanced: Deadline-Aware Retries\n\nStop retrying after absolute deadline, not just attempt count:\n\n```typescript\ninterface DeadlineConfig extends BackoffConfig {\n readonly deadlineMs: number; // Stop all retries by this time\n}\n\nconst exponentialBackoffWithDeadline = (config: DeadlineConfig) =>\n Effect.gen(function* () {\n const startTime = Date.now();\n let attempt = 0;\n\n const isDeadlineExceeded = () =>\n Date.now() - startTime > config.deadlineMs;\n\n while (!isDeadlineExceeded() && attempt < config.maxRetries) {\n const result = yield* flakyApiCall().pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n return result.right;\n }\n\n if (isDeadlineExceeded()) {\n yield* Effect.fail(\n new Error(\n `Deadline exceeded after ${Date.now() - startTime}ms and ${attempt} attempts`\n )\n );\n }\n\n // Calculate delay\n const exponential = config.baseDelayMs * Math.pow(2, attempt);\n const withJitter = exponential * (0.5 + Math.random() * 0.5);\n const delay = Math.min(withJitter, config.maxDelayMs);\n const timeRemaining = config.deadlineMs - (Date.now() - startTime);\n\n // Don't sleep longer than time remaining\n const actualDelay = Math.min(delay, timeRemaining);\n\n yield* Effect.log(\n `[DEADLINE] Attempt ${attempt + 1}: ${Math.round(actualDelay)}ms (${Math.round(\n timeRemaining - actualDelay\n )}ms remaining)`\n );\n\n if (actualDelay > 0) {\n yield* Effect.sleep(`${Math.round(actualDelay)} millis`);\n }\n\n attempt++;\n }\n\n yield* Effect.fail(new Error(\"Max retries exhausted or deadline exceeded\"));\n });\n```\n\n---\n\n## Advanced: Adaptive Backoff Based on Error Type\n\nDifferent backoff strategies for different failure modes:\n\n```typescript\nenum ErrorType {\n Transient = \"transient\", // 503, timeout → backoff helps\n Throttled = \"throttled\", // 429, rate limited → aggressive backoff\n Permanent = \"permanent\", // 400, 401 → don't retry\n}\n\nconst classifyError = (error: Error): ErrorType => {\n const message = error.message;\n\n if (\n message.includes(\"429\") ||\n message.includes(\"too many requests\") ||\n message.includes(\"rate limit\")\n ) {\n return ErrorType.Throttled;\n }\n\n if (\n message.includes(\"503\") ||\n message.includes(\"timeout\") ||\n message.includes(\"temporarily unavailable\")\n ) {\n return ErrorType.Transient;\n }\n\n return ErrorType.Permanent;\n};\n\ninterface AdaptiveBackoffConfig {\n transientConfig: BackoffConfig;\n throttledConfig: BackoffConfig;\n}\n\nconst exponentialBackoffAdaptive = (config: AdaptiveBackoffConfig) =>\n Effect.gen(function* () {\n let attempt = 0;\n let lastError: Error | undefined;\n\n while (true) {\n const result = yield* flakyApiCall().pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n return result.right;\n }\n\n lastError = result.left;\n const errorType = classifyError(lastError);\n\n if (errorType === ErrorType.Permanent) {\n yield* Effect.log(\n `[ERROR] Permanent error, not retrying: ${lastError.message}`\n );\n yield* Effect.fail(lastError);\n }\n\n const backoffConfig =\n errorType === ErrorType.Throttled\n ? config.throttledConfig\n : config.transientConfig;\n\n if (attempt >= backoffConfig.maxRetries) {\n yield* Effect.log(\n `[ERROR] Max retries (${backoffConfig.maxRetries}) exhausted for ${errorType} errors`\n );\n yield* Effect.fail(lastError);\n }\n\n const exponential =\n backoffConfig.baseDelayMs * Math.pow(2, attempt);\n const withJitter = exponential * (0.5 + Math.random() * 0.5);\n const delay = Math.min(withJitter, backoffConfig.maxDelayMs);\n\n yield* Effect.log(\n `[BACKOFF] ${errorType} error (${lastError.message}): ${Math.round(delay)}ms delay`\n );\n\n yield* Effect.sleep(`${Math.round(delay)} millis`);\n\n attempt++;\n }\n });\n\n// Different configs for different error types\nconst adaptiveProgram = exponentialBackoffAdaptive({\n transientConfig: {\n baseDelayMs: 100,\n maxDelayMs: 5000,\n maxRetries: 5,\n },\n throttledConfig: {\n baseDelayMs: 500, // Start longer for throttled\n maxDelayMs: 30000, // Respect rate limiting\n maxRetries: 10, // Retry more for throttled\n },\n});\n```\n\n---\n\n## Advanced: Circuit Breaker with Backoff\n\nCombine exponential backoff with circuit breaker pattern:\n\n```typescript\nenum CircuitState {\n Closed = \"closed\",\n Open = \"open\",\n HalfOpen = \"half-open\",\n}\n\ninterface CircuitBreakerConfig extends BackoffConfig {\n readonly failureThreshold: number; // Failures before opening\n readonly successThreshold: number; // Successes in half-open before closing\n readonly timeout: number; // Time in half-open state\n}\n\nconst exponentialBackoffWithCircuitBreaker = (\n config: CircuitBreakerConfig\n) =>\n Effect.gen(function* () {\n let state = CircuitState.Closed;\n let failureCount = 0;\n let successCount = 0;\n let lastOpenTime = 0;\n let attempt = 0;\n\n while (true) {\n // Check if should transition out of open state\n if (state === CircuitState.Open) {\n const timeSinceOpen = Date.now() - lastOpenTime;\n if (timeSinceOpen > config.timeout) {\n yield* Effect.log(\n `[CIRCUIT] Transitioning to half-open after ${timeSinceOpen}ms`\n );\n state = CircuitState.HalfOpen;\n successCount = 0;\n } else {\n yield* Effect.fail(\n new Error(\n `Circuit breaker open, reopens in ${config.timeout - timeSinceOpen}ms`\n )\n );\n }\n }\n\n if (state === CircuitState.Closed || state === CircuitState.HalfOpen) {\n const result = yield* flakyApiCall().pipe(Effect.either);\n\n if (result._tag === \"Right\") {\n if (state === CircuitState.HalfOpen) {\n successCount++;\n if (successCount >= config.successThreshold) {\n yield* Effect.log(\n `[CIRCUIT] Transitioning to closed (${successCount} successes)`\n );\n state = CircuitState.Closed;\n failureCount = 0;\n }\n }\n return result.right;\n }\n\n failureCount++;\n if (failureCount >= config.failureThreshold) {\n yield* Effect.log(\n `[CIRCUIT] Opening circuit (${failureCount} failures)`\n );\n state = CircuitState.Open;\n lastOpenTime = Date.now();\n } else if (attempt < config.maxRetries) {\n const exponential =\n config.baseDelayMs * Math.pow(2, attempt);\n const delay = Math.min(exponential, config.maxDelayMs);\n yield* Effect.sleep(`${Math.round(delay)} millis`);\n attempt++;\n } else {\n yield* Effect.fail(result.left);\n }\n }\n }\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use exponential backoff when:**\n\n- Retrying failed API calls to external services\n- Implementing resilient microservice communication\n- Recovering from temporary network failures\n- Accessing resources under load or recovery\n- Preventing cascade failures in distributed systems\n- Rate limiting automatic retries\n\n⚠️ **Trade-offs:**\n\n- Adds latency to failure recovery\n- Requires tuning baseDelay and maxDelay\n- Jitter reduces predictability (often desired)\n- Very aggressive backoff can take too long\n\n---\n\n## Backoff Configuration Guide\n\n| Scenario | baseDelay | maxDelay | maxRetries | Notes |\n| --- | --- | --- | --- | --- |\n| Fast API (internal) | 10ms | 1s | 5 | Quick recovery expected |\n| External API | 100ms | 10s | 5 | Slower service recovery |\n| Rate-limited API | 500ms | 60s | 10 | Respect rate limits |\n| Database retry | 50ms | 5s | 3 | Quick local recovery |\n| Third-party service | 1s | 30s | 5 | Conservative for stability |\n\n---\n\n## See Also\n\n- [Retry Effects with Configuration](./retry-effects-with-configuration.mdx) - Configure retry strategies\n- [Handle Errors with Try-Catch Pattern](./handle-errors-with-try-catch-pattern.mdx) - Error handling basics\n- [Understand Failure Handling with Either](./understand-failure-handling-with-either.mdx) - Either/Result pattern\n- [Scheduling Pattern 1: Repeat on Fixed Interval](./scheduling-pattern-repeat-effect-on-fixed-interval.mdx) - Fixed intervals" - }, - { - "id": "scheduling-pattern-cron-expressions", - "title": "Scheduling Pattern 3: Schedule Tasks with Cron Expressions", - "description": "Use cron expressions to schedule periodic tasks at specific calendar times, enabling flexible scheduling beyond simple fixed intervals.", - "skillLevel": "intermediate", - "useCases": [ - "scheduling-periodic-tasks" - ], - "example": "This example demonstrates scheduling a daily report generation using cron, with timezone support.\n\n```typescript\nimport { Effect, Schedule, Console } from \"effect\";\nimport { DateTime } from \"luxon\"; // For timezone handling\n\ninterface ReportConfig {\n readonly cronExpression: string;\n readonly timezone?: string;\n readonly jobName: string;\n}\n\ninterface ScheduledReport {\n readonly timestamp: Date;\n readonly jobName: string;\n readonly result: string;\n}\n\n// Simple cron parser (in production, use a library like cron-parser)\nconst parseCronExpression = (\n expression: string\n): {\n minute: number[];\n hour: number[];\n dayOfMonth: number[];\n month: number[];\n dayOfWeek: number[];\n} => {\n const parts = expression.split(\" \");\n\n const parseField = (field: string, max: number): number[] => {\n if (field === \"*\") {\n return Array.from({ length: max + 1 }, (_, i) => i);\n }\n\n if (field.includes(\",\")) {\n return field.split(\",\").flatMap((part) => parseField(part, max));\n }\n\n if (field.includes(\"-\")) {\n const [start, end] = field.split(\"-\").map(Number);\n return Array.from({ length: end - start + 1 }, (_, i) => start + i);\n }\n\n return [Number(field)];\n };\n\n return {\n minute: parseField(parts[0], 59),\n hour: parseField(parts[1], 23),\n dayOfMonth: parseField(parts[2], 31),\n month: parseField(parts[3], 12),\n dayOfWeek: parseField(parts[4], 6),\n };\n};\n\n// Check if current time matches cron expression\nconst shouldRunNow = (parsed: ReturnType): boolean => {\n const now = new Date();\n\n return (\n parsed.minute.includes(now.getUTCMinutes()) &&\n parsed.hour.includes(now.getUTCHours()) &&\n parsed.dayOfMonth.includes(now.getUTCDate()) &&\n parsed.month.includes(now.getUTCMonth() + 1) &&\n parsed.dayOfWeek.includes(now.getUTCDay())\n );\n};\n\n// Generate a report\nconst generateReport = (jobName: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Console.log(`[REPORT] Generating ${jobName}...`);\n\n // Simulate report generation\n yield* Effect.sleep(\"100 millis\");\n\n return {\n timestamp: new Date(),\n jobName,\n result: `Report generated at ${new Date().toISOString()}`,\n };\n });\n\n// Schedule with cron expression\nconst scheduleWithCron = (config: ReportConfig) =>\n Effect.gen(function* () {\n const parsed = parseCronExpression(config.cronExpression);\n\n yield* Console.log(\n `[SCHEDULER] Scheduling job: ${config.jobName}`\n );\n yield* Console.log(`[SCHEDULER] Cron: ${config.cronExpression}`);\n yield* Console.log(`[SCHEDULER] Timezone: ${config.timezone || \"UTC\"}\\n`);\n\n // Create schedule that checks every minute\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInputEffect((report: ScheduledReport) =>\n Effect.gen(function* () {\n const isPastTime = shouldRunNow(parsed);\n\n if (isPastTime) {\n yield* Console.log(\n `[SCHEDULED] ✓ Running at ${report.timestamp.toISOString()}`\n );\n return true; // Stop scheduling\n }\n\n return false; // Continue scheduling\n })\n )\n );\n\n // Generate report with cron schedule\n yield* generateReport(config.jobName).pipe(\n Effect.repeat(schedule)\n );\n });\n\n// Demonstrate multiple cron schedules\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[START] Scheduling multiple jobs with cron expressions\\n`\n );\n\n // Schedule examples (note: in real app, these would run at actual times)\n const jobs = [\n {\n cronExpression: \"0 9 * * 1-5\", // 9 AM weekdays\n jobName: \"Daily Standup Report\",\n timezone: \"America/New_York\",\n },\n {\n cronExpression: \"0 0 * * *\", // Midnight daily\n jobName: \"Nightly Backup\",\n timezone: \"UTC\",\n },\n {\n cronExpression: \"0 0 1 * *\", // Midnight on 1st of month\n jobName: \"Monthly Summary\",\n timezone: \"Europe/London\",\n },\n ];\n\n yield* Console.log(\"[JOBS] Scheduled:\");\n jobs.forEach((job) => {\n console.log(\n ` - ${job.jobName}: ${job.cronExpression} (${job.timezone})`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Fixed intervals don't align with business needs:\n\n**Fixed interval** (every 24 hours):\n- If task takes 2 hours, next run is 26 hours later\n- Drifts over time\n- No alignment with calendar\n- Fails during daylight saving time changes\n\n**Cron expressions**:\n- Specific calendar times (e.g., always 9 AM)\n- Independent of execution duration\n- Aligns with business hours\n- Natural DST handling (clock adjusts, cron resyncs)\n- Human-readable vs. milliseconds\n\nReal-world example: Daily report at 9 AM\n- **Fixed interval**: Scheduled at 9:00, takes 1 hour → next at 10:00 → drift until 5 PM\n- **Cron `0 9 * * *`**: Always runs at 9:00 regardless of duration or previous delays\n\n---", - "content": "## Guideline\n\nUse cron expressions for scheduling that aligns with business calendars:\n\n- **Hourly backups**: `0 * * * *` (at :00 every hour)\n- **Daily reports**: `0 9 * * 1-5` (9 AM weekdays)\n- **Monthly cleanup**: `0 0 1 * *` (midnight on 1st of month)\n- **Business hours**: `0 9-17 * * 1-5` (9 AM-5 PM, Mon-Fri)\n\nFormat: `minute hour day month weekday`\n\n---\n\n## Rationale\n\nFixed intervals don't align with business needs:\n\n**Fixed interval** (every 24 hours):\n- If task takes 2 hours, next run is 26 hours later\n- Drifts over time\n- No alignment with calendar\n- Fails during daylight saving time changes\n\n**Cron expressions**:\n- Specific calendar times (e.g., always 9 AM)\n- Independent of execution duration\n- Aligns with business hours\n- Natural DST handling (clock adjusts, cron resyncs)\n- Human-readable vs. milliseconds\n\nReal-world example: Daily report at 9 AM\n- **Fixed interval**: Scheduled at 9:00, takes 1 hour → next at 10:00 → drift until 5 PM\n- **Cron `0 9 * * *`**: Always runs at 9:00 regardless of duration or previous delays\n\n---\n\n## Good Example\n\nThis example demonstrates scheduling a daily report generation using cron, with timezone support.\n\n```typescript\nimport { Effect, Schedule, Console } from \"effect\";\nimport { DateTime } from \"luxon\"; // For timezone handling\n\ninterface ReportConfig {\n readonly cronExpression: string;\n readonly timezone?: string;\n readonly jobName: string;\n}\n\ninterface ScheduledReport {\n readonly timestamp: Date;\n readonly jobName: string;\n readonly result: string;\n}\n\n// Simple cron parser (in production, use a library like cron-parser)\nconst parseCronExpression = (\n expression: string\n): {\n minute: number[];\n hour: number[];\n dayOfMonth: number[];\n month: number[];\n dayOfWeek: number[];\n} => {\n const parts = expression.split(\" \");\n\n const parseField = (field: string, max: number): number[] => {\n if (field === \"*\") {\n return Array.from({ length: max + 1 }, (_, i) => i);\n }\n\n if (field.includes(\",\")) {\n return field.split(\",\").flatMap((part) => parseField(part, max));\n }\n\n if (field.includes(\"-\")) {\n const [start, end] = field.split(\"-\").map(Number);\n return Array.from({ length: end - start + 1 }, (_, i) => start + i);\n }\n\n return [Number(field)];\n };\n\n return {\n minute: parseField(parts[0], 59),\n hour: parseField(parts[1], 23),\n dayOfMonth: parseField(parts[2], 31),\n month: parseField(parts[3], 12),\n dayOfWeek: parseField(parts[4], 6),\n };\n};\n\n// Check if current time matches cron expression\nconst shouldRunNow = (parsed: ReturnType): boolean => {\n const now = new Date();\n\n return (\n parsed.minute.includes(now.getUTCMinutes()) &&\n parsed.hour.includes(now.getUTCHours()) &&\n parsed.dayOfMonth.includes(now.getUTCDate()) &&\n parsed.month.includes(now.getUTCMonth() + 1) &&\n parsed.dayOfWeek.includes(now.getUTCDay())\n );\n};\n\n// Generate a report\nconst generateReport = (jobName: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Console.log(`[REPORT] Generating ${jobName}...`);\n\n // Simulate report generation\n yield* Effect.sleep(\"100 millis\");\n\n return {\n timestamp: new Date(),\n jobName,\n result: `Report generated at ${new Date().toISOString()}`,\n };\n });\n\n// Schedule with cron expression\nconst scheduleWithCron = (config: ReportConfig) =>\n Effect.gen(function* () {\n const parsed = parseCronExpression(config.cronExpression);\n\n yield* Console.log(\n `[SCHEDULER] Scheduling job: ${config.jobName}`\n );\n yield* Console.log(`[SCHEDULER] Cron: ${config.cronExpression}`);\n yield* Console.log(`[SCHEDULER] Timezone: ${config.timezone || \"UTC\"}\\n`);\n\n // Create schedule that checks every minute\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInputEffect((report: ScheduledReport) =>\n Effect.gen(function* () {\n const isPastTime = shouldRunNow(parsed);\n\n if (isPastTime) {\n yield* Console.log(\n `[SCHEDULED] ✓ Running at ${report.timestamp.toISOString()}`\n );\n return true; // Stop scheduling\n }\n\n return false; // Continue scheduling\n })\n )\n );\n\n // Generate report with cron schedule\n yield* generateReport(config.jobName).pipe(\n Effect.repeat(schedule)\n );\n });\n\n// Demonstrate multiple cron schedules\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[START] Scheduling multiple jobs with cron expressions\\n`\n );\n\n // Schedule examples (note: in real app, these would run at actual times)\n const jobs = [\n {\n cronExpression: \"0 9 * * 1-5\", // 9 AM weekdays\n jobName: \"Daily Standup Report\",\n timezone: \"America/New_York\",\n },\n {\n cronExpression: \"0 0 * * *\", // Midnight daily\n jobName: \"Nightly Backup\",\n timezone: \"UTC\",\n },\n {\n cronExpression: \"0 0 1 * *\", // Midnight on 1st of month\n jobName: \"Monthly Summary\",\n timezone: \"Europe/London\",\n },\n ];\n\n yield* Console.log(\"[JOBS] Scheduled:\");\n jobs.forEach((job) => {\n console.log(\n ` - ${job.jobName}: ${job.cronExpression} (${job.timezone})`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Timezone-Aware Cron Scheduling\n\nHandle daylight saving time transitions and different timezones:\n\n```typescript\ninterface TimezoneConfig extends ReportConfig {\n readonly timezone: string;\n}\n\nconst scheduleWithTimezone = (config: TimezoneConfig) =>\n Effect.gen(function* () {\n yield* Console.log(`[TIMEZONE] Scheduling in ${config.timezone}`);\n\n // Get current time in specified timezone\n const getTimeInTimezone = (): Date => {\n const formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: config.timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n\n const parts = formatter.formatToParts(new Date());\n const date = new Date();\n\n // Reconstruct date in timezone\n return date;\n };\n\n // Check if should run (in specified timezone)\n const shouldRunInTimezone = (parsed: ReturnType) => {\n const now = getTimeInTimezone();\n\n return (\n parsed.minute.includes(now.getMinutes()) &&\n parsed.hour.includes(now.getHours())\n );\n };\n\n const parsed = parseCronExpression(config.cronExpression);\n\n // Schedule task\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInput((prev: number) => {\n const shouldRun = shouldRunInTimezone(parsed);\n\n if (shouldRun) {\n yield* Console.log(\n `[TIMEZONE] Running job in ${config.timezone} at ${getTimeInTimezone().toISOString()}`\n );\n return true;\n }\n\n return false;\n })\n );\n\n yield* generateReport(config.jobName).pipe(\n Effect.repeat(schedule)\n );\n });\n```\n\n---\n\n## Advanced: Cron with Overlapping Execution Prevention\n\nPrevent multiple instances of the same job from running:\n\n```typescript\ninterface CronJobWithLocking {\n readonly jobName: string;\n readonly cronExpression: string;\n readonly maxDurationMs: number;\n}\n\nconst scheduleWithLocking = (config: CronJobWithLocking) =>\n Effect.gen(function* () {\n let isRunning = false;\n let lastCompletionTime = 0;\n\n const parsed = parseCronExpression(config.cronExpression);\n\n yield* Console.log(\n `[LOCKING] Job: ${config.jobName}, max duration: ${config.maxDurationMs}ms`\n );\n\n // Schedule with lock\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInputEffect((report: ScheduledReport) =>\n Effect.gen(function* () {\n const now = Date.now();\n const shouldRun = shouldRunNow(parsed);\n const isStale = now - lastCompletionTime > config.maxDurationMs * 2;\n\n if (!shouldRun) {\n return false;\n }\n\n if (isRunning && !isStale) {\n yield* Console.log(\n `[LOCKING] Job already running, skipping duplicate`\n );\n return false;\n }\n\n if (isStale && isRunning) {\n yield* Console.log(\n `[LOCKING] Previous instance stale, allowing restart`\n );\n }\n\n isRunning = true;\n\n try {\n const startTime = Date.now();\n\n yield* generateReport(config.jobName).pipe(\n Effect.timeout(`${config.maxDurationMs} millis`)\n );\n\n const duration = Date.now() - startTime;\n lastCompletionTime = Date.now();\n\n yield* Console.log(\n `[LOCKING] Job completed in ${duration}ms`\n );\n } catch (error) {\n yield* Console.log(\n `[LOCKING] Job failed: ${error}`\n );\n } finally {\n isRunning = false;\n }\n\n return false; // Continue scheduling\n })\n )\n );\n\n yield* Schedule.repeat_forever(schedule);\n });\n```\n\n---\n\n## Advanced: Dynamic Cron Expression Changes\n\nUpdate cron expressions at runtime without restarting:\n\n```typescript\nimport { Ref } from \"effect\";\n\ninterface DynamicCronConfig {\n readonly jobName: string;\n readonly initialCron: string;\n}\n\nconst scheduleDynamicCron = (config: DynamicCronConfig) =>\n Effect.gen(function* () {\n const cronExpression = yield* Ref.make(config.initialCron);\n\n yield* Console.log(\n `[DYNAMIC] Job: ${config.jobName}, initial cron: ${config.initialCron}`\n );\n\n // Method to update cron expression\n const updateCronExpression = (newExpression: string) =>\n Effect.gen(function* () {\n const old = yield* Ref.get(cronExpression);\n yield* Ref.set(cronExpression, newExpression);\n yield* Console.log(\n `[DYNAMIC] Updated cron: ${old} → ${newExpression}`\n );\n });\n\n // Schedule with dynamic cron\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInputEffect((report: ScheduledReport) =>\n Effect.gen(function* () {\n const currentCron = yield* Ref.get(cronExpression);\n const parsed = parseCronExpression(currentCron);\n\n if (shouldRunNow(parsed)) {\n yield* Console.log(\n `[DYNAMIC] Running with: ${currentCron}`\n );\n return true;\n }\n\n return false;\n })\n )\n );\n\n // Run job\n yield* generateReport(config.jobName).pipe(\n Effect.repeat(schedule)\n );\n });\n\n// Example: Update cron every hour\nconst updateCronPeriodically = (\n jobName: string,\n schedule: { time: string; cron: string }[]\n) =>\n Effect.gen(function* () {\n const currentHour = new Date().getHours();\n const nextSchedule = schedule[currentHour % schedule.length];\n\n // This is a simplified example\n yield* Console.log(\n `[UPDATE] New schedule for hour ${currentHour}: ${nextSchedule.cron}`\n );\n });\n```\n\n---\n\n## Advanced: Cron with Backoff and Retry\n\nCombine cron scheduling with exponential backoff:\n\n```typescript\ninterface CronWithRetryConfig extends CronJobWithLocking {\n readonly maxRetries: number;\n readonly baseDelayMs: number;\n readonly maxDelayMs: number;\n}\n\nconst scheduleCronWithBackoff = (config: CronWithRetryConfig) =>\n Effect.gen(function* () {\n const parsed = parseCronExpression(config.cronExpression);\n\n yield* Console.log(\n `[CRON+RETRY] Job: ${config.jobName}, max retries: ${config.maxRetries}`\n );\n\n const schedule = Schedule.fixed(\"1 minute\").pipe(\n Schedule.untilInputEffect((report: ScheduledReport) =>\n Effect.gen(function* () {\n if (!shouldRunNow(parsed)) {\n return false;\n }\n\n // Try to execute with backoff\n let attempt = 0;\n let lastError: Error | undefined;\n\n while (attempt < config.maxRetries) {\n try {\n yield* generateReport(config.jobName);\n return false; // Success, continue scheduling\n } catch (error) {\n lastError = error as Error;\n\n if (attempt < config.maxRetries - 1) {\n const exponential = config.baseDelayMs * Math.pow(2, attempt);\n const delay = Math.min(exponential, config.maxDelayMs);\n\n yield* Console.log(\n `[RETRY] Attempt ${attempt + 1} failed, retrying in ${delay}ms`\n );\n\n yield* Effect.sleep(`${delay} millis`);\n }\n\n attempt++;\n }\n }\n\n if (lastError) {\n yield* Console.log(\n `[FAILURE] Job failed after ${config.maxRetries} attempts: ${lastError.message}`\n );\n }\n\n return false; // Continue scheduling for next cron time\n })\n )\n );\n\n yield* Schedule.repeat_forever(schedule);\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use cron expressions when:**\n\n- Running daily/hourly/weekly tasks at specific times\n- Business logic tied to clock time (daily reports at 9 AM)\n- Calendar-based operations (month-end close, quarterly reviews)\n- Need alignment with business hours or timezones\n- Long-term recurring tasks\n- Tasks that should survive application restarts\n\n⚠️ **Trade-offs:**\n\n- More complex than fixed intervals\n- Cron parsing can be CPU intensive (cache results)\n- DST transitions require careful handling\n- Timezone library dependencies\n- Distributed systems need coordination (prevent duplicates)\n\n---\n\n## Common Cron Expressions\n\n| Expression | Meaning |\n| --- | --- |\n| `0 0 * * *` | Every day at midnight |\n| `0 9 * * 1-5` | 9 AM Monday-Friday |\n| `0 12 * * 0,6` | Noon on weekends |\n| `*/15 * * * *` | Every 15 minutes |\n| `0 0 1 * *` | First day of month |\n| `0 0 * * 0` | Every Sunday |\n| `0 9,17 * * *` | 9 AM and 5 PM daily |\n| `30 2 * * *` | 2:30 AM daily |\n\n---\n\n## See Also\n\n- [Scheduling Pattern 1: Repeat on Fixed Interval](./scheduling-pattern-repeat-effect-on-fixed-interval.mdx) - Fixed interval scheduling\n- [Scheduling Pattern 2: Exponential Backoff](./scheduling-pattern-exponential-backoff.mdx) - Retry with backoff\n- [Run Background Tasks with Fork](./run-background-tasks-with-fork.mdx) - Background execution\n- [Handle Side Effects with Effect.sync](./handle-side-effects-with-effect-sync.mdx) - Effect side effects" - }, - { - "id": "scheduling-pattern-debounce-throttle", - "title": "Scheduling Pattern 4: Debounce and Throttle Execution", - "description": "Use debounce to wait for silence before executing, and throttle to limit execution frequency, both critical for handling rapid events.", - "skillLevel": "intermediate", - "useCases": [ - "scheduling-periodic-tasks" - ], - "example": "This example demonstrates debouncing and throttling for common scenarios.\n\n```typescript\nimport { Effect, Schedule, Ref } from \"effect\";\n\ninterface SearchQuery {\n readonly query: string;\n readonly timestamp: Date;\n}\n\n// Simulate API search\nconst performSearch = (query: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[API] Searching for: \"${query}\"`);\n\n yield* Effect.sleep(\"100 millis\"); // Simulate API delay\n\n return [\n `Result 1 for ${query}`,\n `Result 2 for ${query}`,\n `Result 3 for ${query}`,\n ];\n });\n\n// Main: demonstrate debounce and throttle\nconst program = Effect.gen(function* () {\n console.log(`\\n[DEBOUNCE/THROTTLE] Handling rapid events\\n`);\n\n // Example 1: Debounce search input\n console.log(`[1] Debounced search (wait for silence):\\n`);\n\n const searchQueries = [\"h\", \"he\", \"hel\", \"hell\", \"hello\"];\n\n const debouncedSearches = yield* Ref.make[]>([]);\n\n for (const query of searchQueries) {\n yield* Effect.log(`[INPUT] User typed: \"${query}\"`);\n\n // In real app, this would be debounced\n yield* Effect.sleep(\"150 millis\"); // User typing\n }\n\n // After user stops, execute search\n yield* Effect.log(`[DEBOUNCE] User silent for 200ms, executing search`);\n\n const searchResults = yield* performSearch(\"hello\");\n\n yield* Effect.log(`[RESULTS] ${searchResults.length} results found\\n`);\n\n // Example 2: Throttle scroll events\n console.log(`[2] Throttled scroll handler (max 10/sec):\\n`);\n\n const scrollEventCount = yield* Ref.make(0);\n const updateCount = yield* Ref.make(0);\n\n // Simulate 100 rapid scroll events\n for (let i = 0; i < 100; i++) {\n yield* Ref.update(scrollEventCount, (c) => c + 1);\n\n // In real app, scroll handler would be throttled\n if (i % 10 === 0) {\n // Simulate throttled update (max 10 per second)\n yield* Ref.update(updateCount, (c) => c + 1);\n }\n }\n\n const events = yield* Ref.get(scrollEventCount);\n const updates = yield* Ref.get(updateCount);\n\n yield* Effect.log(\n `[THROTTLE] ${events} scroll events → ${updates} updates (${(updates / events * 100).toFixed(1)}% update rate)\\n`\n );\n\n // Example 3: Deduplication\n console.log(`[3] Deduplicating rapid events:\\n`);\n\n const userClicks = [\"click\", \"click\", \"click\", \"dblclick\", \"click\"];\n\n const lastClick = yield* Ref.make(null);\n const clickCount = yield* Ref.make(0);\n\n for (const click of userClicks) {\n const prev = yield* Ref.get(lastClick);\n\n if (click !== prev) {\n yield* Effect.log(`[CLICK] Processing: ${click}`);\n yield* Ref.update(clickCount, (c) => c + 1);\n yield* Ref.set(lastClick, click);\n } else {\n yield* Effect.log(`[CLICK] Duplicate: ${click} (skipped)`);\n }\n }\n\n const processed = yield* Ref.get(clickCount);\n\n yield* Effect.log(\n `\\n[DEDUPE] ${userClicks.length} clicks → ${processed} processed\\n`\n );\n\n // Example 4: Exponential backoff on repeated errors\n console.log(`[4] Throttled retry on errors:\\n`);\n\n let retryCount = 0;\n\n const operation = Effect.gen(function* () {\n retryCount++;\n\n if (retryCount < 3) {\n yield* Effect.fail(new Error(\"Still failing\"));\n }\n\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${retryCount}`);\n\n return \"done\";\n }).pipe(\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.upTo(\"1 second\"),\n Schedule.recurs(5)\n )\n )\n );\n\n yield* operation;\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Rapid events without debounce/throttle cause problems:\n\n**Debounce example**: Search input\n- User types \"hello\" character by character\n- Without debounce: 5 API calls (one per character)\n- With debounce: 1 API call after user stops typing\n\n**Throttle example**: Scroll events\n- Scroll fires 100+ times per second\n- Without throttle: Updates lag, GC pressure\n- With throttle: Update max 60 times per second\n\nReal-world issues:\n- **API overload**: Search queries hammer backend\n- **Rendering lag**: Too many DOM updates\n- **Resource exhaustion**: Event handlers never catch up\n\nDebounce/throttle enable:\n- **Efficiency**: Fewer operations\n- **Responsiveness**: UI stays smooth\n- **Resource safety**: Prevent exhaustion\n- **Sanity**: Predictable execution\n\n---", - "content": "## Guideline\n\nDebounce and throttle manage rapid events:\n\n- **Debounce**: Wait for silence (delay after last event), then execute once\n- **Throttle**: Execute at most once per interval\n- **Deduplication**: Skip duplicate events\n- **Rate limiting**: Limit events per second\n\nPattern: `Schedule.debounce(duration)` or `Schedule.throttle(maxEvents, duration)`\n\n---\n\n## Rationale\n\nRapid events without debounce/throttle cause problems:\n\n**Debounce example**: Search input\n- User types \"hello\" character by character\n- Without debounce: 5 API calls (one per character)\n- With debounce: 1 API call after user stops typing\n\n**Throttle example**: Scroll events\n- Scroll fires 100+ times per second\n- Without throttle: Updates lag, GC pressure\n- With throttle: Update max 60 times per second\n\nReal-world issues:\n- **API overload**: Search queries hammer backend\n- **Rendering lag**: Too many DOM updates\n- **Resource exhaustion**: Event handlers never catch up\n\nDebounce/throttle enable:\n- **Efficiency**: Fewer operations\n- **Responsiveness**: UI stays smooth\n- **Resource safety**: Prevent exhaustion\n- **Sanity**: Predictable execution\n\n---\n\n## Good Example\n\nThis example demonstrates debouncing and throttling for common scenarios.\n\n```typescript\nimport { Effect, Schedule, Ref } from \"effect\";\n\ninterface SearchQuery {\n readonly query: string;\n readonly timestamp: Date;\n}\n\n// Simulate API search\nconst performSearch = (query: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.log(`[API] Searching for: \"${query}\"`);\n\n yield* Effect.sleep(\"100 millis\"); // Simulate API delay\n\n return [\n `Result 1 for ${query}`,\n `Result 2 for ${query}`,\n `Result 3 for ${query}`,\n ];\n });\n\n// Main: demonstrate debounce and throttle\nconst program = Effect.gen(function* () {\n console.log(`\\n[DEBOUNCE/THROTTLE] Handling rapid events\\n`);\n\n // Example 1: Debounce search input\n console.log(`[1] Debounced search (wait for silence):\\n`);\n\n const searchQueries = [\"h\", \"he\", \"hel\", \"hell\", \"hello\"];\n\n const debouncedSearches = yield* Ref.make[]>([]);\n\n for (const query of searchQueries) {\n yield* Effect.log(`[INPUT] User typed: \"${query}\"`);\n\n // In real app, this would be debounced\n yield* Effect.sleep(\"150 millis\"); // User typing\n }\n\n // After user stops, execute search\n yield* Effect.log(`[DEBOUNCE] User silent for 200ms, executing search`);\n\n const searchResults = yield* performSearch(\"hello\");\n\n yield* Effect.log(`[RESULTS] ${searchResults.length} results found\\n`);\n\n // Example 2: Throttle scroll events\n console.log(`[2] Throttled scroll handler (max 10/sec):\\n`);\n\n const scrollEventCount = yield* Ref.make(0);\n const updateCount = yield* Ref.make(0);\n\n // Simulate 100 rapid scroll events\n for (let i = 0; i < 100; i++) {\n yield* Ref.update(scrollEventCount, (c) => c + 1);\n\n // In real app, scroll handler would be throttled\n if (i % 10 === 0) {\n // Simulate throttled update (max 10 per second)\n yield* Ref.update(updateCount, (c) => c + 1);\n }\n }\n\n const events = yield* Ref.get(scrollEventCount);\n const updates = yield* Ref.get(updateCount);\n\n yield* Effect.log(\n `[THROTTLE] ${events} scroll events → ${updates} updates (${(updates / events * 100).toFixed(1)}% update rate)\\n`\n );\n\n // Example 3: Deduplication\n console.log(`[3] Deduplicating rapid events:\\n`);\n\n const userClicks = [\"click\", \"click\", \"click\", \"dblclick\", \"click\"];\n\n const lastClick = yield* Ref.make(null);\n const clickCount = yield* Ref.make(0);\n\n for (const click of userClicks) {\n const prev = yield* Ref.get(lastClick);\n\n if (click !== prev) {\n yield* Effect.log(`[CLICK] Processing: ${click}`);\n yield* Ref.update(clickCount, (c) => c + 1);\n yield* Ref.set(lastClick, click);\n } else {\n yield* Effect.log(`[CLICK] Duplicate: ${click} (skipped)`);\n }\n }\n\n const processed = yield* Ref.get(clickCount);\n\n yield* Effect.log(\n `\\n[DEDUPE] ${userClicks.length} clicks → ${processed} processed\\n`\n );\n\n // Example 4: Exponential backoff on repeated errors\n console.log(`[4] Throttled retry on errors:\\n`);\n\n let retryCount = 0;\n\n const operation = Effect.gen(function* () {\n retryCount++;\n\n if (retryCount < 3) {\n yield* Effect.fail(new Error(\"Still failing\"));\n }\n\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${retryCount}`);\n\n return \"done\";\n }).pipe(\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.upTo(\"1 second\"),\n Schedule.recurs(5)\n )\n )\n );\n\n yield* operation;\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Custom Debounce Implementation\n\nBuild your own debounce with Ref:\n\n```typescript\nconst createDebounced = (\n handler: (value: A) => Effect.Effect,\n delayMs: number\n) =>\n Effect.gen(function* () {\n let timeoutId: NodeJS.Timeout | null = null;\n const lastValue = yield* Ref.make(null);\n\n return (value: A) =>\n Effect.gen(function* () {\n yield* Ref.set(lastValue, value);\n\n // Clear previous timeout\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n\n // Set new timeout\n return new Promise((resolve, reject) => {\n timeoutId = setTimeout(\n () => {\n handler(value).pipe(\n Effect.runPromise\n ).then(resolve).catch(reject);\n },\n delayMs\n );\n });\n });\n });\n\n// Usage\nconst debouncedSearch = createDebounced(\n (query: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`[DEBOUNCED] Searching: ${query}`);\n return [\"result1\", \"result2\"];\n }),\n 300 // Wait 300ms after last input\n);\n```\n\n---\n\n## Advanced: Throttle with Burst Allowance\n\nAllow occasional bursts while throttling:\n\n```typescript\nconst createThrottled = (\n handler: (value: A) => Effect.Effect,\n config: {\n maxPerSec: number;\n burstSize: number;\n }\n) =>\n Effect.gen(function* () {\n let tokens = config.burstSize;\n let lastRefillTime = Date.now();\n\n return (value: A) =>\n Effect.gen(function* () {\n const now = Date.now();\n const timeSinceRefill = (now - lastRefillTime) / 1000;\n\n // Refill tokens based on time passed\n tokens = Math.min(\n config.burstSize,\n tokens + timeSinceRefill * config.maxPerSec\n );\n\n lastRefillTime = now;\n\n if (tokens >= 1) {\n tokens--;\n yield* handler(value);\n } else {\n const waitTime = (1 - tokens) / config.maxPerSec * 1000;\n\n yield* Effect.log(\n `[THROTTLE] Rate limited, waiting ${waitTime.toFixed(0)}ms`\n );\n\n yield* Effect.sleep(`${Math.ceil(waitTime)} millis`);\n yield* handler(value);\n }\n });\n });\n\n// Usage: Allow 10/sec with bursts of 5\nconst throttledAPI = createThrottled(\n (request: string) =>\n Effect.log(`[REQUEST] ${request}`),\n { maxPerSec: 10, burstSize: 5 }\n);\n```\n\n---\n\n## Advanced: Adaptive Debounce\n\nIncrease debounce delay if events keep coming:\n\n```typescript\nconst adaptiveDebounce = (\n handler: (value: A) => Effect.Effect,\n config: {\n initialDelayMs: number;\n maxDelayMs: number;\n increaseMs: number;\n }\n) =>\n Effect.gen(function* () {\n let currentDelay = config.initialDelayMs;\n let timeoutId: NodeJS.Timeout | null = null;\n\n return (value: A) =>\n Effect.gen(function* () {\n if (timeoutId) {\n // Event came while waiting, increase delay\n clearTimeout(timeoutId);\n\n currentDelay = Math.min(\n config.maxDelayMs,\n currentDelay + config.increaseMs\n );\n\n yield* Effect.log(\n `[ADAPTIVE] Increased debounce to ${currentDelay}ms`\n );\n } else {\n // First event, reset delay\n currentDelay = config.initialDelayMs;\n }\n\n return new Promise((resolve, reject) => {\n timeoutId = setTimeout(\n () => {\n handler(value).pipe(\n Effect.runPromise\n ).then(resolve).catch(reject);\n\n timeoutId = null;\n currentDelay = config.initialDelayMs; // Reset for next batch\n },\n currentDelay\n );\n });\n });\n });\n\n// Usage: Search that waits longer if user keeps typing\nconst adaptiveSearch = adaptiveDebounce(\n performSearch,\n { initialDelayMs: 100, maxDelayMs: 500, increaseMs: 100 }\n);\n```\n\n---\n\n## Advanced: Deduplicate Consecutive Values\n\nSkip duplicate events in sequence:\n\n```typescript\nconst deduplicateConsecutive = (\n stream: Effect.Effect,\n equals: (a: A, b: A) => boolean = (a, b) => a === b\n) =>\n Effect.gen(function* () {\n let lastValue: A | undefined;\n const deduplicated: A[] = [];\n\n const values = yield* stream;\n\n for (const value of values) {\n if (lastValue === undefined || !equals(value, lastValue)) {\n deduplicated.push(value);\n lastValue = value;\n }\n }\n\n return deduplicated;\n });\n\n// Usage: Remove duplicate clicks\nconst dedupeClicks = deduplicateConsecutive(\n Effect.succeed([\"click\", \"click\", \"dblclick\", \"click\"]),\n (a, b) => a === b\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use debounce when:**\n\n- Search/filter input\n- Auto-save functionality\n- Window resize/scroll handlers\n- Form validation\n- API calls should wait for silence\n\n✅ **Use throttle when:**\n\n- Scroll events\n- Mouse move tracking\n- API rate limiting\n- Real-time updates\n- High-frequency events\n\n⚠️ **Trade-offs:**\n\n- Debounce adds latency (user waits)\n- Throttle misses some events\n- Tuning delays requires testing\n- Complex state management\n\n---\n\n## Debounce vs Throttle\n\n| Aspect | Debounce | Throttle |\n| --- | --- | --- |\n| **Trigger** | After silence | At intervals |\n| **Use Case** | Search input | Scroll tracking |\n| **Latency** | High | Low |\n| **Events Lost** | Many | None guaranteed |\n| **Best For** | Bursty traffic | Continuous stream |\n\n---\n\n## See Also\n\n- [Stream Pattern 3: Backpressure Control](./stream-pattern-backpressure-control.mdx) - Backpressure handling\n- [Concurrency Pattern 2: Rate Limit with Semaphore](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Rate limiting\n- [Scheduling Pattern 2: Exponential Backoff](./scheduling-pattern-exponential-backoff.mdx) - Retry with backoff\n- [Control Repetition with Schedule](./control-repetition-with-schedule.mdx) - Schedule fundamentals" - }, - { - "id": "scheduling-pattern-advanced-retry-chains", - "title": "Scheduling Pattern 5: Advanced Retry Chains and Circuit Breakers", - "description": "Use retry chains with circuit breakers to handle complex failure scenarios, detect cascade failures early, and prevent resource exhaustion.", - "skillLevel": "advanced", - "useCases": [ - "scheduling-periodic-tasks" - ], - "example": "This example demonstrates circuit breaker and fallback chain patterns.\n\n```typescript\nimport { Effect, Schedule, Ref, Data } from \"effect\";\n\n// Error classification\nclass RetryableError extends Data.TaggedError(\"RetryableError\")<{\n message: string;\n code: string;\n}> {}\n\nclass NonRetryableError extends Data.TaggedError(\"NonRetryableError\")<{\n message: string;\n code: string;\n}> {}\n\nclass CircuitBreakerOpenError extends Data.TaggedError(\"CircuitBreakerOpenError\")<{\n message: string;\n}> {}\n\n// Circuit breaker state\ninterface CircuitBreakerState {\n status: \"closed\" | \"open\" | \"half-open\";\n failureCount: number;\n lastFailureTime: Date | null;\n successCount: number;\n}\n\n// Create circuit breaker\nconst createCircuitBreaker = (config: {\n failureThreshold: number;\n resetTimeoutMs: number;\n halfOpenRequests: number;\n}) =>\n Effect.gen(function* () {\n const state = yield* Ref.make({\n status: \"closed\",\n failureCount: 0,\n lastFailureTime: null,\n successCount: 0,\n });\n\n const recordSuccess = Effect.gen(function* () {\n yield* Ref.modify(state, (s) => {\n if (s.status === \"half-open\") {\n return [\n undefined,\n {\n ...s,\n successCount: s.successCount + 1,\n status: s.successCount + 1 >= config.halfOpenRequests\n ? \"closed\"\n : \"half-open\",\n failureCount: 0,\n },\n ];\n }\n return [undefined, s];\n });\n });\n\n const recordFailure = Effect.gen(function* () {\n yield* Ref.modify(state, (s) => {\n const newFailureCount = s.failureCount + 1;\n const newStatus = newFailureCount >= config.failureThreshold\n ? \"open\"\n : s.status;\n\n return [\n undefined,\n {\n ...s,\n failureCount: newFailureCount,\n lastFailureTime: new Date(),\n status: newStatus,\n },\n ];\n });\n });\n\n const canExecute = Effect.gen(function* () {\n const current = yield* Ref.get(state);\n\n if (current.status === \"closed\") {\n return true;\n }\n\n if (current.status === \"open\") {\n const timeSinceFailure = Date.now() - (current.lastFailureTime?.getTime() ?? 0);\n\n if (timeSinceFailure > config.resetTimeoutMs) {\n yield* Ref.modify(state, (s) => [\n undefined,\n {\n ...s,\n status: \"half-open\",\n failureCount: 0,\n successCount: 0,\n },\n ]);\n return true;\n }\n\n return false;\n }\n\n // half-open: allow limited requests\n return true;\n });\n\n return { recordSuccess, recordFailure, canExecute, state };\n });\n\n// Main example\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED RETRY] Circuit breaker and fallback chains\\n`);\n\n // Create circuit breaker\n const cb = yield* createCircuitBreaker({\n failureThreshold: 3,\n resetTimeoutMs: 1000,\n halfOpenRequests: 2,\n });\n\n // Example 1: Circuit breaker in action\n console.log(`[1] Circuit breaker state transitions:\\n`);\n\n let requestCount = 0;\n\n const callWithCircuitBreaker = (shouldFail: boolean) =>\n Effect.gen(function* () {\n const canExecute = yield* cb.canExecute;\n\n if (!canExecute) {\n yield* Effect.fail(\n new CircuitBreakerOpenError({\n message: \"Circuit breaker is open\",\n })\n );\n }\n\n requestCount++;\n\n if (shouldFail) {\n yield* cb.recordFailure;\n yield* Effect.log(\n `[REQUEST ${requestCount}] FAILED (Circuit: ${(yield* Ref.get(cb.state)).status})`\n );\n yield* Effect.fail(\n new RetryableError({\n message: \"Service error\",\n code: \"500\",\n })\n );\n } else {\n yield* cb.recordSuccess;\n yield* Effect.log(\n `[REQUEST ${requestCount}] SUCCESS (Circuit: ${(yield* Ref.get(cb.state)).status})`\n );\n return \"success\";\n }\n });\n\n // Simulate failures then recovery\n const failSequence = [true, true, true, false, false, false];\n\n for (const shouldFail of failSequence) {\n yield* callWithCircuitBreaker(shouldFail).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error._tag === \"CircuitBreakerOpenError\") {\n yield* Effect.log(\n `[REQUEST ${requestCount + 1}] REJECTED (Circuit open)`\n );\n } else {\n yield* Effect.log(\n `[REQUEST ${requestCount + 1}] ERROR caught`\n );\n }\n })\n )\n );\n\n // Add delay between requests\n yield* Effect.sleep(\"100 millis\");\n }\n\n // Example 2: Fallback chain\n console.log(`\\n[2] Fallback chain (primary → secondary → cache):\\n`);\n\n const endpoints = {\n primary: \"https://api.primary.com/data\",\n secondary: \"https://api.secondary.com/data\",\n cache: \"cached-data\",\n };\n\n const callEndpoint = (name: string, shouldFail: boolean) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CALL] Trying ${name}`);\n\n if (shouldFail) {\n yield* Effect.sleep(\"50 millis\");\n yield* Effect.fail(\n new RetryableError({\n message: `${name} failed`,\n code: \"500\",\n })\n );\n }\n\n yield* Effect.sleep(\"50 millis\");\n return `data-from-${name}`;\n });\n\n const fallbackChain = callEndpoint(\"primary\", true).pipe(\n Effect.orElse(() => callEndpoint(\"secondary\", false)),\n Effect.orElse(() => {\n yield* Effect.log(`[FALLBACK] Using cached data`);\n return Effect.succeed(endpoints.cache);\n })\n );\n\n const result = yield* fallbackChain;\n\n yield* Effect.log(`[RESULT] Got: ${result}\\n`);\n\n // Example 3: Error-specific retry strategy\n console.log(`[3] Error classification and adaptive retry:\\n`);\n\n const classifyError = (code: string) => {\n if ([\"502\", \"503\", \"504\"].includes(code)) {\n return \"retryable-service-error\";\n }\n if ([\"408\", \"429\"].includes(code)) {\n return \"retryable-rate-limit\";\n }\n if ([\"404\", \"401\", \"403\"].includes(code)) {\n return \"non-retryable\";\n }\n if (code === \"timeout\") {\n return \"retryable-network\";\n }\n return \"unknown\";\n };\n\n const errorCodes = [\"500\", \"404\", \"429\", \"503\", \"timeout\"];\n\n for (const code of errorCodes) {\n const classification = classifyError(code);\n const shouldRetry = !classification.startsWith(\"non-retryable\");\n\n yield* Effect.log(\n `[ERROR ${code}] → ${classification} (Retry: ${shouldRetry})`\n );\n }\n\n // Example 4: Bulkhead pattern\n console.log(`\\n[4] Bulkhead isolation (limit concurrency per endpoint):\\n`);\n\n const bulkheads = {\n \"primary-api\": { maxConcurrent: 5, currentCount: 0 },\n \"secondary-api\": { maxConcurrent: 3, currentCount: 0 },\n };\n\n const acquirePermit = (endpoint: string) =>\n Effect.gen(function* () {\n const bulkhead = bulkheads[endpoint as keyof typeof bulkheads];\n\n if (!bulkhead) {\n return false;\n }\n\n if (bulkhead.currentCount < bulkhead.maxConcurrent) {\n bulkhead.currentCount++;\n return true;\n }\n\n yield* Effect.log(\n `[BULKHEAD] ${endpoint} at capacity (${bulkhead.currentCount}/${bulkhead.maxConcurrent})`\n );\n\n return false;\n });\n\n // Simulate requests\n for (let i = 0; i < 10; i++) {\n const endpoint = i < 6 ? \"primary-api\" : \"secondary-api\";\n const acquired = yield* acquirePermit(endpoint);\n\n if (acquired) {\n yield* Effect.log(\n `[REQUEST] Acquired permit for ${endpoint}`\n );\n }\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Simple retry fails in production:\n\n**Scenario 1: Cascade Failure**\n- Service A calls Service B (down)\n- Retries pile up, consuming resources\n- A gets overloaded trying to recover B\n- System collapses\n\n**Scenario 2: Mixed Failures**\n- 404 (not found) - retrying won't help\n- 500 (server error) - retrying might help\n- Network timeout - retrying might help\n- Same retry strategy for all = inefficient\n\n**Scenario 3: Thundering Herd**\n- 10,000 clients all retrying at once\n- Server recovers, gets hammered again\n- Needs coordinated backoff + jitter\n\nSolutions:\n\n**Circuit breaker**:\n- Monitor error rate\n- Stop requests when high\n- Resume gradually\n- Prevent cascade failures\n\n**Fallback chain**:\n- Try primary endpoint\n- Try secondary endpoint\n- Use cache\n- Return degraded result\n\n**Adaptive retry**:\n- Classify error type\n- Use appropriate strategy\n- Skip unretryable errors\n- Adjust backoff dynamically\n\n---", - "content": "## Guideline\n\nAdvanced retry strategies handle multiple failure types:\n\n- **Circuit breaker**: Stop retrying when error rate is high\n- **Bulkhead**: Limit concurrency per operation\n- **Fallback chain**: Try multiple approaches in order\n- **Adaptive retry**: Adjust strategy based on failure pattern\n- **Health checks**: Verify recovery before resuming\n\nPattern: Combine `Schedule.retry`, `Ref` state, and error classification\n\n---\n\n## Rationale\n\nSimple retry fails in production:\n\n**Scenario 1: Cascade Failure**\n- Service A calls Service B (down)\n- Retries pile up, consuming resources\n- A gets overloaded trying to recover B\n- System collapses\n\n**Scenario 2: Mixed Failures**\n- 404 (not found) - retrying won't help\n- 500 (server error) - retrying might help\n- Network timeout - retrying might help\n- Same retry strategy for all = inefficient\n\n**Scenario 3: Thundering Herd**\n- 10,000 clients all retrying at once\n- Server recovers, gets hammered again\n- Needs coordinated backoff + jitter\n\nSolutions:\n\n**Circuit breaker**:\n- Monitor error rate\n- Stop requests when high\n- Resume gradually\n- Prevent cascade failures\n\n**Fallback chain**:\n- Try primary endpoint\n- Try secondary endpoint\n- Use cache\n- Return degraded result\n\n**Adaptive retry**:\n- Classify error type\n- Use appropriate strategy\n- Skip unretryable errors\n- Adjust backoff dynamically\n\n---\n\n## Good Example\n\nThis example demonstrates circuit breaker and fallback chain patterns.\n\n```typescript\nimport { Effect, Schedule, Ref, Data } from \"effect\";\n\n// Error classification\nclass RetryableError extends Data.TaggedError(\"RetryableError\")<{\n message: string;\n code: string;\n}> {}\n\nclass NonRetryableError extends Data.TaggedError(\"NonRetryableError\")<{\n message: string;\n code: string;\n}> {}\n\nclass CircuitBreakerOpenError extends Data.TaggedError(\"CircuitBreakerOpenError\")<{\n message: string;\n}> {}\n\n// Circuit breaker state\ninterface CircuitBreakerState {\n status: \"closed\" | \"open\" | \"half-open\";\n failureCount: number;\n lastFailureTime: Date | null;\n successCount: number;\n}\n\n// Create circuit breaker\nconst createCircuitBreaker = (config: {\n failureThreshold: number;\n resetTimeoutMs: number;\n halfOpenRequests: number;\n}) =>\n Effect.gen(function* () {\n const state = yield* Ref.make({\n status: \"closed\",\n failureCount: 0,\n lastFailureTime: null,\n successCount: 0,\n });\n\n const recordSuccess = Effect.gen(function* () {\n yield* Ref.modify(state, (s) => {\n if (s.status === \"half-open\") {\n return [\n undefined,\n {\n ...s,\n successCount: s.successCount + 1,\n status: s.successCount + 1 >= config.halfOpenRequests\n ? \"closed\"\n : \"half-open\",\n failureCount: 0,\n },\n ];\n }\n return [undefined, s];\n });\n });\n\n const recordFailure = Effect.gen(function* () {\n yield* Ref.modify(state, (s) => {\n const newFailureCount = s.failureCount + 1;\n const newStatus = newFailureCount >= config.failureThreshold\n ? \"open\"\n : s.status;\n\n return [\n undefined,\n {\n ...s,\n failureCount: newFailureCount,\n lastFailureTime: new Date(),\n status: newStatus,\n },\n ];\n });\n });\n\n const canExecute = Effect.gen(function* () {\n const current = yield* Ref.get(state);\n\n if (current.status === \"closed\") {\n return true;\n }\n\n if (current.status === \"open\") {\n const timeSinceFailure = Date.now() - (current.lastFailureTime?.getTime() ?? 0);\n\n if (timeSinceFailure > config.resetTimeoutMs) {\n yield* Ref.modify(state, (s) => [\n undefined,\n {\n ...s,\n status: \"half-open\",\n failureCount: 0,\n successCount: 0,\n },\n ]);\n return true;\n }\n\n return false;\n }\n\n // half-open: allow limited requests\n return true;\n });\n\n return { recordSuccess, recordFailure, canExecute, state };\n });\n\n// Main example\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED RETRY] Circuit breaker and fallback chains\\n`);\n\n // Create circuit breaker\n const cb = yield* createCircuitBreaker({\n failureThreshold: 3,\n resetTimeoutMs: 1000,\n halfOpenRequests: 2,\n });\n\n // Example 1: Circuit breaker in action\n console.log(`[1] Circuit breaker state transitions:\\n`);\n\n let requestCount = 0;\n\n const callWithCircuitBreaker = (shouldFail: boolean) =>\n Effect.gen(function* () {\n const canExecute = yield* cb.canExecute;\n\n if (!canExecute) {\n yield* Effect.fail(\n new CircuitBreakerOpenError({\n message: \"Circuit breaker is open\",\n })\n );\n }\n\n requestCount++;\n\n if (shouldFail) {\n yield* cb.recordFailure;\n yield* Effect.log(\n `[REQUEST ${requestCount}] FAILED (Circuit: ${(yield* Ref.get(cb.state)).status})`\n );\n yield* Effect.fail(\n new RetryableError({\n message: \"Service error\",\n code: \"500\",\n })\n );\n } else {\n yield* cb.recordSuccess;\n yield* Effect.log(\n `[REQUEST ${requestCount}] SUCCESS (Circuit: ${(yield* Ref.get(cb.state)).status})`\n );\n return \"success\";\n }\n });\n\n // Simulate failures then recovery\n const failSequence = [true, true, true, false, false, false];\n\n for (const shouldFail of failSequence) {\n yield* callWithCircuitBreaker(shouldFail).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error._tag === \"CircuitBreakerOpenError\") {\n yield* Effect.log(\n `[REQUEST ${requestCount + 1}] REJECTED (Circuit open)`\n );\n } else {\n yield* Effect.log(\n `[REQUEST ${requestCount + 1}] ERROR caught`\n );\n }\n })\n )\n );\n\n // Add delay between requests\n yield* Effect.sleep(\"100 millis\");\n }\n\n // Example 2: Fallback chain\n console.log(`\\n[2] Fallback chain (primary → secondary → cache):\\n`);\n\n const endpoints = {\n primary: \"https://api.primary.com/data\",\n secondary: \"https://api.secondary.com/data\",\n cache: \"cached-data\",\n };\n\n const callEndpoint = (name: string, shouldFail: boolean) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CALL] Trying ${name}`);\n\n if (shouldFail) {\n yield* Effect.sleep(\"50 millis\");\n yield* Effect.fail(\n new RetryableError({\n message: `${name} failed`,\n code: \"500\",\n })\n );\n }\n\n yield* Effect.sleep(\"50 millis\");\n return `data-from-${name}`;\n });\n\n const fallbackChain = callEndpoint(\"primary\", true).pipe(\n Effect.orElse(() => callEndpoint(\"secondary\", false)),\n Effect.orElse(() => {\n yield* Effect.log(`[FALLBACK] Using cached data`);\n return Effect.succeed(endpoints.cache);\n })\n );\n\n const result = yield* fallbackChain;\n\n yield* Effect.log(`[RESULT] Got: ${result}\\n`);\n\n // Example 3: Error-specific retry strategy\n console.log(`[3] Error classification and adaptive retry:\\n`);\n\n const classifyError = (code: string) => {\n if ([\"502\", \"503\", \"504\"].includes(code)) {\n return \"retryable-service-error\";\n }\n if ([\"408\", \"429\"].includes(code)) {\n return \"retryable-rate-limit\";\n }\n if ([\"404\", \"401\", \"403\"].includes(code)) {\n return \"non-retryable\";\n }\n if (code === \"timeout\") {\n return \"retryable-network\";\n }\n return \"unknown\";\n };\n\n const errorCodes = [\"500\", \"404\", \"429\", \"503\", \"timeout\"];\n\n for (const code of errorCodes) {\n const classification = classifyError(code);\n const shouldRetry = !classification.startsWith(\"non-retryable\");\n\n yield* Effect.log(\n `[ERROR ${code}] → ${classification} (Retry: ${shouldRetry})`\n );\n }\n\n // Example 4: Bulkhead pattern\n console.log(`\\n[4] Bulkhead isolation (limit concurrency per endpoint):\\n`);\n\n const bulkheads = {\n \"primary-api\": { maxConcurrent: 5, currentCount: 0 },\n \"secondary-api\": { maxConcurrent: 3, currentCount: 0 },\n };\n\n const acquirePermit = (endpoint: string) =>\n Effect.gen(function* () {\n const bulkhead = bulkheads[endpoint as keyof typeof bulkheads];\n\n if (!bulkhead) {\n return false;\n }\n\n if (bulkhead.currentCount < bulkhead.maxConcurrent) {\n bulkhead.currentCount++;\n return true;\n }\n\n yield* Effect.log(\n `[BULKHEAD] ${endpoint} at capacity (${bulkhead.currentCount}/${bulkhead.maxConcurrent})`\n );\n\n return false;\n });\n\n // Simulate requests\n for (let i = 0; i < 10; i++) {\n const endpoint = i < 6 ? \"primary-api\" : \"secondary-api\";\n const acquired = yield* acquirePermit(endpoint);\n\n if (acquired) {\n yield* Effect.log(\n `[REQUEST] Acquired permit for ${endpoint}`\n );\n }\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Complex Retry Strategy\n\nCombine multiple retry conditions:\n\n```typescript\nconst createComplexRetryStrategy = (config: {\n maxAttempts: number;\n baseDelayMs: number;\n maxDelayMs: number;\n timeoutMs: number;\n}) =>\n Schedule.recurse((attempt) => {\n if (attempt >= config.maxAttempts) {\n return Schedule.stop;\n }\n\n // Exponential backoff with jitter\n const delay = Math.min(\n config.maxDelayMs,\n config.baseDelayMs * Math.pow(2, attempt) +\n Math.random() * config.baseDelayMs\n );\n\n // Add jitter to prevent thundering herd\n const jitter = Math.random() * 0.1 * delay;\n\n return Schedule.delay(\n Duration.millis(Math.ceil(delay + jitter))\n );\n }).pipe(\n Schedule.upTo(Duration.millis(config.timeoutMs)),\n Schedule.recurs(config.maxAttempts)\n );\n\n// Usage with error filtering\nconst robustCall = operation.pipe(\n Effect.retry(\n createComplexRetryStrategy({\n maxAttempts: 5,\n baseDelayMs: 100,\n maxDelayMs: 5000,\n timeoutMs: 30000,\n }).pipe(\n Schedule.filter((error) => {\n // Don't retry non-retryable errors\n if (error instanceof NonRetryableError) {\n return false;\n }\n return true;\n })\n )\n )\n);\n```\n\n---\n\n## Advanced: Health Check with Recovery\n\nVerify recovery before resuming traffic:\n\n```typescript\nconst createHealthAwareRetry = (config: {\n maxRetries: number;\n healthCheckDelayMs: number;\n healthCheckTimeoutMs: number;\n}) =>\n Effect.gen(function* () {\n const isHealthy = yield* Ref.make(true);\n\n const performHealthCheck = Effect.gen(function* () {\n yield* Effect.log(\"[HEALTH] Checking service health\");\n\n // Replace with actual health check\n const healthy = Math.random() > 0.3;\n\n yield* Ref.set(isHealthy, healthy);\n yield* Effect.log(\n `[HEALTH] Service is ${healthy ? \"healthy\" : \"still failing\"}`\n );\n\n return healthy;\n }).pipe(\n Effect.timeout(Duration.millis(config.healthCheckTimeoutMs)),\n Effect.catchAll(() => {\n yield* Effect.log(\"[HEALTH] Check timed out, assuming unhealthy\");\n return Effect.succeed(false);\n })\n );\n\n return performHealthCheck;\n });\n```\n\n---\n\n## Advanced: Telemetry and Observability\n\nTrack retry metrics for debugging:\n\n```typescript\ninterface RetryMetrics {\n attempts: number;\n failures: number;\n totalDelayMs: number;\n errorCodes: string[];\n lastError: Error | null;\n}\n\nconst trackRetryMetrics = (operation: Effect.Effect) =>\n Effect.gen(function* () {\n const metrics = yield* Ref.make({\n attempts: 0,\n failures: 0,\n totalDelayMs: 0,\n errorCodes: [],\n lastError: null,\n });\n\n const result = yield* operation.pipe(\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.compose(\n Schedule.recurs(3),\n Schedule.tapOutput((delay) =>\n Effect.gen(function* () {\n yield* Ref.modify(metrics, (m) => [\n undefined,\n {\n ...m,\n attempts: m.attempts + 1,\n totalDelayMs: m.totalDelayMs + delay.millis,\n },\n ]);\n })\n )\n )\n )\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Ref.modify(metrics, (m) => [\n undefined,\n {\n ...m,\n failures: m.failures + 1,\n lastError: error instanceof Error ? error : new Error(String(error)),\n },\n ]);\n\n return Effect.fail(error);\n })\n )\n );\n\n const final = yield* Ref.get(metrics);\n\n yield* Effect.log(\n `[METRICS] Attempts: ${final.attempts}, Failures: ${final.failures}, Total delay: ${final.totalDelayMs}ms`\n );\n\n return result;\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use circuit breaker when:**\n- Calling external services\n- Error rate might spike\n- Want to fail fast\n- Protecting dependent services\n\n✅ **Use fallback chain when:**\n- Multiple approaches available\n- Want graceful degradation\n- Have secondary endpoints\n- Need cache as last resort\n\n✅ **Use adaptive retry when:**\n- Mixed error types expected\n- Different errors need different strategies\n- Want to optimize retry efficiency\n- Observability matters\n\n⚠️ **Trade-offs:**\n- Complexity increases significantly\n- More state to manage\n- Requires careful tuning\n- Harder to reason about\n\n---\n\n## Configuration Guide\n\n| Setting | Recommendation | Notes |\n| --- | --- | --- |\n| **Failure Threshold** | 5-10 errors | Too low = circuit opens incorrectly |\n| **Reset Timeout** | 30-60 seconds | Too short = rapid oscillation |\n| **Half-Open Requests** | 2-5 | Verify recovery gradually |\n| **Max Retries** | 3-5 | Balance between resilience and delay |\n| **Max Delay** | 10-30 seconds | Prevent long tail latencies |\n\n---\n\n## See Also\n\n- [Scheduling Pattern 2: Exponential Backoff](./scheduling-pattern-exponential-backoff.mdx) - Backoff basics\n- [Scheduling Pattern 4: Debounce and Throttle](./scheduling-pattern-debounce-throttle.mdx) - Rate limiting\n- [Error Handling Pattern 1: Accumulation](./error-handling-pattern-accumulation.mdx) - Multiple error handling\n- [Error Handling Pattern 3: Custom Error Strategies](./error-handling-pattern-custom-strategies.mdx) - Error classification" - }, - { - "id": "send-json-response", - "title": "Send a JSON Response", - "description": "Use Http.response.json to automatically serialize data structures into a JSON response.", - "skillLevel": "beginner", - "useCases": [ - "building-apis" - ], - "example": "This example defines a route that fetches a user object and returns it as a JSON response. The `Http.response.json` function handles all the necessary serialization and header configuration.\n\n```typescript\nimport { Effect, Context, Duration, Layer } from \"effect\";\nimport { NodeContext, NodeHttpServer } from \"@effect/platform-node\";\nimport { createServer } from \"node:http\";\n\nconst PORT = 3459; // Changed port to avoid conflicts\n\n// Define HTTP Server service\nclass JsonServer extends Effect.Service()(\"JsonServer\", {\n sync: () => ({\n handleRequest: () =>\n Effect.succeed({\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n message: \"Hello, JSON!\",\n timestamp: new Date().toISOString(),\n }),\n }),\n }),\n}) {}\n\n// Create and run the server\nconst program = Effect.gen(function* () {\n const jsonServer = yield* JsonServer;\n\n // Create and start HTTP server\n const server = createServer((req, res) => {\n const requestHandler = Effect.gen(function* () {\n try {\n const response = yield* jsonServer.handleRequest();\n res.writeHead(response.status, response.headers);\n res.end(response.body);\n // Log the response for demonstration\n yield* Effect.logInfo(`Sent JSON response: ${response.body}`);\n } catch (error: any) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n yield* Effect.logError(`Request error: ${error.message}`);\n }\n });\n\n Effect.runPromise(requestHandler);\n });\n\n // Start server with error handling\n yield* Effect.async((resume) => {\n server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));\n } else {\n resume(Effect.fail(error));\n }\n });\n\n server.listen(PORT, () => {\n resume(Effect.succeed(void 0));\n });\n });\n\n yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);\n yield* Effect.logInfo(\"Try: curl http://localhost:3459\");\n\n // Run for a short time to demonstrate\n yield* Effect.sleep(Duration.seconds(3));\n\n // Shutdown gracefully\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shutdown complete\");\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Server error: ${error.message}`);\n return error;\n })\n ),\n // Merge layers and provide them in a single call to ensure proper lifecycle management\n Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))\n);\n\n// Run the program\n// Use Effect.runFork for server applications that shouldn't resolve the promise\nEffect.runPromise(\n program.pipe(\n // Ensure the Effect has no remaining context requirements for runPromise\n Effect.map(() => undefined)\n )\n);\n```", - "antiPattern": "The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst getUserRoute = Http.router.get(\n \"/users/1\",\n Effect.succeed({ id: 1, name: \"Paul\", team: \"Effect\" }).pipe(\n Effect.flatMap((user) => {\n // Manually serialize the object to a JSON string.\n const jsonString = JSON.stringify(user);\n // Create a text response with the string.\n const response = Http.response.text(jsonString);\n // Manually set the Content-Type header.\n return Effect.succeed(\n Http.response.setHeader(\n response,\n \"Content-Type\",\n \"application/json; charset=utf-8\"\n )\n );\n })\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `Http.response.json` helper eliminates this entire class of potential bugs.", - "explanation": "APIs predominantly communicate using JSON. The `Http` module provides a dedicated `Http.response.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.\n\nUsing `Http.response.json` is superior because:\n\n1. **Automatic Serialization**: It safely handles the `JSON.stringify` operation for you, including handling potential circular references or other serialization errors.\n2. **Correct Headers**: It automatically sets the `Content-Type: application/json; charset=utf-8` header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs.\n3. **Simplicity and Readability**: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP.\n4. **Composability**: It creates a standard `Http.response` object that works seamlessly with all other parts of the Effect `Http` module.\n\n---", - "content": "## Guideline\n\nTo return a JavaScript object or value as a JSON response, use the `Http.response.json(data)` constructor.\n\n---\n\n## Rationale\n\nAPIs predominantly communicate using JSON. The `Http` module provides a dedicated `Http.response.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.\n\nUsing `Http.response.json` is superior because:\n\n1. **Automatic Serialization**: It safely handles the `JSON.stringify` operation for you, including handling potential circular references or other serialization errors.\n2. **Correct Headers**: It automatically sets the `Content-Type: application/json; charset=utf-8` header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs.\n3. **Simplicity and Readability**: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP.\n4. **Composability**: It creates a standard `Http.response` object that works seamlessly with all other parts of the Effect `Http` module.\n\n---\n\n## Good Example\n\nThis example defines a route that fetches a user object and returns it as a JSON response. The `Http.response.json` function handles all the necessary serialization and header configuration.\n\n```typescript\nimport { Effect, Context, Duration, Layer } from \"effect\";\nimport { NodeContext, NodeHttpServer } from \"@effect/platform-node\";\nimport { createServer } from \"node:http\";\n\nconst PORT = 3459; // Changed port to avoid conflicts\n\n// Define HTTP Server service\nclass JsonServer extends Effect.Service()(\"JsonServer\", {\n sync: () => ({\n handleRequest: () =>\n Effect.succeed({\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n message: \"Hello, JSON!\",\n timestamp: new Date().toISOString(),\n }),\n }),\n }),\n}) {}\n\n// Create and run the server\nconst program = Effect.gen(function* () {\n const jsonServer = yield* JsonServer;\n\n // Create and start HTTP server\n const server = createServer((req, res) => {\n const requestHandler = Effect.gen(function* () {\n try {\n const response = yield* jsonServer.handleRequest();\n res.writeHead(response.status, response.headers);\n res.end(response.body);\n // Log the response for demonstration\n yield* Effect.logInfo(`Sent JSON response: ${response.body}`);\n } catch (error: any) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n yield* Effect.logError(`Request error: ${error.message}`);\n }\n });\n\n Effect.runPromise(requestHandler);\n });\n\n // Start server with error handling\n yield* Effect.async((resume) => {\n server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));\n } else {\n resume(Effect.fail(error));\n }\n });\n\n server.listen(PORT, () => {\n resume(Effect.succeed(void 0));\n });\n });\n\n yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);\n yield* Effect.logInfo(\"Try: curl http://localhost:3459\");\n\n // Run for a short time to demonstrate\n yield* Effect.sleep(Duration.seconds(3));\n\n // Shutdown gracefully\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shutdown complete\");\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Server error: ${error.message}`);\n return error;\n })\n ),\n // Merge layers and provide them in a single call to ensure proper lifecycle management\n Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))\n);\n\n// Run the program\n// Use Effect.runFork for server applications that shouldn't resolve the promise\nEffect.runPromise(\n program.pipe(\n // Ensure the Effect has no remaining context requirements for runPromise\n Effect.map(() => undefined)\n )\n);\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst getUserRoute = Http.router.get(\n \"/users/1\",\n Effect.succeed({ id: 1, name: \"Paul\", team: \"Effect\" }).pipe(\n Effect.flatMap((user) => {\n // Manually serialize the object to a JSON string.\n const jsonString = JSON.stringify(user);\n // Create a text response with the string.\n const response = Http.response.text(jsonString);\n // Manually set the Content-Type header.\n return Effect.succeed(\n Http.response.setHeader(\n response,\n \"Content-Type\",\n \"application/json; charset=utf-8\"\n )\n );\n })\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `Http.response.json` helper eliminates this entire class of potential bugs." - }, - { - "id": "combinator-sequencing", - "title": "Sequencing with andThen, tap, and flatten", - "description": "Use sequencing combinators to run computations in order, perform side effects, or flatten nested structures, while preserving error and context handling.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// andThen: Run one effect, then another, ignore the first result\nconst logThenCompute = Effect.log(\"Starting...\").pipe(\n Effect.andThen(Effect.succeed(42))\n); // Effect\n\n// tap: Log the result of an effect, but keep the value\nconst computeAndLog = Effect.succeed(42).pipe(\n Effect.tap((n) => Effect.log(`Result is ${n}`))\n); // Effect\n\n// flatten: Remove one level of nesting\nconst nestedOption = Option.some(Option.some(1));\nconst flatOption = Option.flatten(nestedOption); // Option\n\nconst nestedEffect = Effect.succeed(Effect.succeed(1));\nconst flatEffect = Effect.flatten(nestedEffect); // Effect\n\n// tapError: Log errors without handling them\nconst mightFail = Effect.fail(\"fail!\").pipe(\n Effect.tapError((err) => Effect.logError(`Error: ${err}`))\n); // Effect\n\n// Stream: tap for side effects on each element\nconst stream = Stream.fromIterable([1, 2, 3]).pipe(\n Stream.tap((n) => Effect.log(`Saw: ${n}`))\n); // Stream\n```\n\n**Explanation:**\n\n- `andThen` is for sequencing when you don’t care about the first result.\n- `tap` is for running side effects (like logging) without changing the value.\n- `flatten` is for removing unnecessary nesting (e.g., `Option>` → `Option`).", - "antiPattern": "Using `flatMap` with a function that ignores its argument, or manually unwrapping and re-wrapping nested structures, instead of using the dedicated combinators.", - "explanation": "Sequencing is fundamental for expressing workflows. \nThese combinators let you:\n\n- Run computations in order (`andThen`)\n- Attach logging, metrics, or other side effects (`tap`)\n- Simplify nested structures (`flatten`)\n\nAll while preserving composability, error handling, and type safety.", - "content": "# Sequencing with `andThen`, `tap`, and `flatten`\n\n## Guideline\n\nUse sequencing combinators to run computations in order, perform side effects, or flatten nested structures.\n\n- `andThen` runs one computation after another, ignoring the first result.\n- `tap` runs a side-effecting computation with the result, without changing the value.\n- `flatten` removes one level of nesting from nested structures.\n\nThese work for `Effect`, `Stream`, `Option`, and `Either`.\n\n## Rationale\n\nSequencing is fundamental for expressing workflows. \nThese combinators let you:\n\n- Run computations in order (`andThen`)\n- Attach logging, metrics, or other side effects (`tap`)\n- Simplify nested structures (`flatten`)\n\nAll while preserving composability, error handling, and type safety.\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// andThen: Run one effect, then another, ignore the first result\nconst logThenCompute = Effect.log(\"Starting...\").pipe(\n Effect.andThen(Effect.succeed(42))\n); // Effect\n\n// tap: Log the result of an effect, but keep the value\nconst computeAndLog = Effect.succeed(42).pipe(\n Effect.tap((n) => Effect.log(`Result is ${n}`))\n); // Effect\n\n// flatten: Remove one level of nesting\nconst nestedOption = Option.some(Option.some(1));\nconst flatOption = Option.flatten(nestedOption); // Option\n\nconst nestedEffect = Effect.succeed(Effect.succeed(1));\nconst flatEffect = Effect.flatten(nestedEffect); // Effect\n\n// tapError: Log errors without handling them\nconst mightFail = Effect.fail(\"fail!\").pipe(\n Effect.tapError((err) => Effect.logError(`Error: ${err}`))\n); // Effect\n\n// Stream: tap for side effects on each element\nconst stream = Stream.fromIterable([1, 2, 3]).pipe(\n Stream.tap((n) => Effect.log(`Saw: ${n}`))\n); // Stream\n```\n\n**Explanation:**\n\n- `andThen` is for sequencing when you don’t care about the first result.\n- `tap` is for running side effects (like logging) without changing the value.\n- `flatten` is for removing unnecessary nesting (e.g., `Option>` → `Option`).\n\n## Anti-Pattern\n\nUsing `flatMap` with a function that ignores its argument, or manually unwrapping and re-wrapping nested structures, instead of using the dedicated combinators." - }, - { - "id": "setup-new-project", - "title": "Set Up a New Effect Project", - "description": "Set up a new Effect project.", - "skillLevel": "beginner", - "useCases": [ - "project-setup--execution" - ], - "example": "```typescript\n// 1. Init project (e.g., `npm init -y`)\n// 2. Install deps (e.g., `npm install effect`, `npm install -D typescript tsx`)\n// 3. Create tsconfig.json with `\"strict\": true`\n// 4. Create src/index.ts\nimport { Effect } from \"effect\";\n\nconst program = Effect.log(\"Hello, World!\");\n\nEffect.runSync(program);\n\n// 5. Run the program (e.g., `npx tsx src/index.ts`)\n```\n\n**Explanation:** \nThis setup ensures you have TypeScript and Effect ready to go, with strict\ntype-checking for maximum safety and correctness.", - "antiPattern": "Avoid disabling `strict` mode in your `tsconfig.json`. Running with\n`\"strict\": false` will cause you to lose many of the type-safety guarantees\nthat make Effect so powerful.", - "explanation": "A proper setup is crucial for leveraging Effect's powerful type-safety\nfeatures. Using TypeScript's `strict` mode is non-negotiable.", - "content": "# Set Up a New Effect Project\n\n## Guideline\n\nTo start a new Effect project, initialize a standard Node.js project, add\n`effect` and `typescript` as dependencies, and create a `tsconfig.json` file\nwith strict mode enabled.\n\n## Rationale\n\nA proper setup is crucial for leveraging Effect's powerful type-safety\nfeatures. Using TypeScript's `strict` mode is non-negotiable.\n\n## Good Example\n\n```typescript\n// 1. Init project (e.g., `npm init -y`)\n// 2. Install deps (e.g., `npm install effect`, `npm install -D typescript tsx`)\n// 3. Create tsconfig.json with `\"strict\": true`\n// 4. Create src/index.ts\nimport { Effect } from \"effect\";\n\nconst program = Effect.log(\"Hello, World!\");\n\nEffect.runSync(program);\n\n// 5. Run the program (e.g., `npx tsx src/index.ts`)\n```\n\n**Explanation:** \nThis setup ensures you have TypeScript and Effect ready to go, with strict\ntype-checking for maximum safety and correctness.\n\n## Anti-Pattern\n\nAvoid disabling `strict` mode in your `tsconfig.json`. Running with\n`\"strict\": false` will cause you to lose many of the type-safety guarantees\nthat make Effect so powerful." - }, - { - "id": "observability-alerting", - "title": "Set Up Alerting", - "description": "Create alerts based on SLOs and symptoms, not causes.", - "skillLevel": "advanced", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, Metric, Schedule, Duration, Ref } from \"effect\"\n\n// ============================================\n// 1. Define alertable conditions\n// ============================================\n\ninterface Alert {\n readonly name: string\n readonly severity: \"critical\" | \"warning\" | \"info\"\n readonly message: string\n readonly timestamp: Date\n readonly labels: Record\n}\n\ninterface AlertRule {\n readonly name: string\n readonly condition: Effect.Effect\n readonly severity: \"critical\" | \"warning\" | \"info\"\n readonly message: string\n readonly labels: Record\n readonly forDuration: Duration.DurationInput\n}\n\n// ============================================\n// 2. Define alert rules\n// ============================================\n\nconst createAlertRules = (metrics: {\n errorRate: () => Effect.Effect\n latencyP99: () => Effect.Effect\n availability: () => Effect.Effect\n}): AlertRule[] => [\n {\n name: \"HighErrorRate\",\n condition: metrics.errorRate().pipe(Effect.map((rate) => rate > 0.01)),\n severity: \"critical\",\n message: \"Error rate exceeds 1%\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"5 minutes\",\n },\n {\n name: \"HighLatency\",\n condition: metrics.latencyP99().pipe(Effect.map((p99) => p99 > 2)),\n severity: \"warning\",\n message: \"P99 latency exceeds 2 seconds\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"10 minutes\",\n },\n {\n name: \"LowAvailability\",\n condition: metrics.availability().pipe(Effect.map((avail) => avail < 99.9)),\n severity: \"critical\",\n message: \"Availability below 99.9% SLO\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"5 minutes\",\n },\n {\n name: \"ErrorBudgetLow\",\n condition: Effect.succeed(false), // Implement based on error budget calc\n severity: \"warning\",\n message: \"Error budget below 25%\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"0 seconds\",\n },\n]\n\n// ============================================\n// 3. Alert manager\n// ============================================\n\ninterface AlertState {\n readonly firing: Map\n readonly resolved: Alert[]\n}\n\nconst makeAlertManager = Effect.gen(function* () {\n const state = yield* Ref.make({\n firing: new Map(),\n resolved: [],\n })\n\n const checkRule = (rule: AlertRule) =>\n Effect.gen(function* () {\n const isTriggered = yield* rule.condition\n\n yield* Ref.modify(state, (s) => {\n const firing = new Map(s.firing)\n const resolved = [...s.resolved]\n const key = rule.name\n\n if (isTriggered) {\n if (!firing.has(key)) {\n // New alert\n firing.set(key, {\n since: new Date(),\n alert: {\n name: rule.name,\n severity: rule.severity,\n message: rule.message,\n timestamp: new Date(),\n labels: rule.labels,\n },\n })\n }\n } else {\n if (firing.has(key)) {\n // Alert resolved\n const prev = firing.get(key)!\n resolved.push({\n ...prev.alert,\n message: `[RESOLVED] ${prev.alert.message}`,\n timestamp: new Date(),\n })\n firing.delete(key)\n }\n }\n\n return [undefined, { firing, resolved }]\n })\n })\n\n const getActiveAlerts = () =>\n Ref.get(state).pipe(\n Effect.map((s) => Array.from(s.firing.values()).map((f) => f.alert))\n )\n\n const getRecentResolved = () =>\n Ref.get(state).pipe(Effect.map((s) => s.resolved.slice(-10)))\n\n return {\n checkRule,\n getActiveAlerts,\n getRecentResolved,\n }\n})\n\n// ============================================\n// 4. Alert notification\n// ============================================\n\ninterface NotificationChannel {\n readonly send: (alert: Alert) => Effect.Effect\n}\n\nconst slackChannel: NotificationChannel = {\n send: (alert) =>\n Effect.gen(function* () {\n const emoji =\n alert.severity === \"critical\"\n ? \"🔴\"\n : alert.severity === \"warning\"\n ? \"🟡\"\n : \"🔵\"\n\n yield* Effect.log(`${emoji} [${alert.severity.toUpperCase()}] ${alert.name}`).pipe(\n Effect.annotateLogs({\n message: alert.message,\n labels: JSON.stringify(alert.labels),\n })\n )\n\n // In real implementation: call Slack API\n }),\n}\n\nconst pagerDutyChannel: NotificationChannel = {\n send: (alert) =>\n Effect.gen(function* () {\n if (alert.severity === \"critical\") {\n yield* Effect.log(\"PagerDuty: Creating incident\").pipe(\n Effect.annotateLogs({ alert: alert.name })\n )\n // In real implementation: call PagerDuty API\n }\n }),\n}\n\n// ============================================\n// 5. Alert evaluation loop\n// ============================================\n\nconst runAlertEvaluation = (\n rules: AlertRule[],\n channels: NotificationChannel[],\n interval: Duration.DurationInput\n) =>\n Effect.gen(function* () {\n const alertManager = yield* makeAlertManager\n const previousAlerts = yield* Ref.make(new Set())\n\n yield* Effect.forever(\n Effect.gen(function* () {\n // Check all rules\n for (const rule of rules) {\n yield* alertManager.checkRule(rule)\n }\n\n // Get current active alerts\n const active = yield* alertManager.getActiveAlerts()\n const current = new Set(active.map((a) => a.name))\n const previous = yield* Ref.get(previousAlerts)\n\n // Find newly firing alerts\n for (const alert of active) {\n if (!previous.has(alert.name)) {\n // New alert - send notifications\n for (const channel of channels) {\n yield* channel.send(alert)\n }\n }\n }\n\n yield* Ref.set(previousAlerts, current)\n yield* Effect.sleep(interval)\n })\n )\n })\n\n// ============================================\n// 6. Prometheus alerting rules (YAML)\n// ============================================\n\nconst prometheusAlertRules = `\ngroups:\n - name: effect-app-alerts\n rules:\n - alert: HighErrorRate\n expr: |\n sum(rate(http_errors_total[5m]))\n /\n sum(rate(http_requests_total[5m]))\n > 0.01\n for: 5m\n labels:\n severity: critical\n annotations:\n summary: \"High error rate detected\"\n description: \"Error rate is {{ $value | humanizePercentage }}\"\n\n - alert: HighLatency\n expr: |\n histogram_quantile(0.99,\n sum(rate(http_request_duration_seconds_bucket[5m])) by (le)\n ) > 2\n for: 10m\n labels:\n severity: warning\n annotations:\n summary: \"High P99 latency\"\n description: \"P99 latency is {{ $value }}s\"\n\n - alert: SLOViolation\n expr: |\n sum(rate(http_requests_total{status!~\"5..\"}[30m]))\n /\n sum(rate(http_requests_total[30m]))\n < 0.999\n for: 5m\n labels:\n severity: critical\n annotations:\n summary: \"SLO violation\"\n description: \"Availability is {{ $value | humanizePercentage }}\"\n`\n```", - "antiPattern": "", - "explanation": "Good alerting:\n\n1. **Catches real problems** - Alerts when users are affected\n2. **Reduces noise** - Fewer false positives\n3. **Enables response** - Actionable information\n4. **Supports SLOs** - Tracks service level objectives\n\n---", - "content": "## Guideline\n\nSet up alerts based on user-facing symptoms (SLO violations) rather than system metrics (CPU usage).\n\n---\n\n## Rationale\n\nGood alerting:\n\n1. **Catches real problems** - Alerts when users are affected\n2. **Reduces noise** - Fewer false positives\n3. **Enables response** - Actionable information\n4. **Supports SLOs** - Tracks service level objectives\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Metric, Schedule, Duration, Ref } from \"effect\"\n\n// ============================================\n// 1. Define alertable conditions\n// ============================================\n\ninterface Alert {\n readonly name: string\n readonly severity: \"critical\" | \"warning\" | \"info\"\n readonly message: string\n readonly timestamp: Date\n readonly labels: Record\n}\n\ninterface AlertRule {\n readonly name: string\n readonly condition: Effect.Effect\n readonly severity: \"critical\" | \"warning\" | \"info\"\n readonly message: string\n readonly labels: Record\n readonly forDuration: Duration.DurationInput\n}\n\n// ============================================\n// 2. Define alert rules\n// ============================================\n\nconst createAlertRules = (metrics: {\n errorRate: () => Effect.Effect\n latencyP99: () => Effect.Effect\n availability: () => Effect.Effect\n}): AlertRule[] => [\n {\n name: \"HighErrorRate\",\n condition: metrics.errorRate().pipe(Effect.map((rate) => rate > 0.01)),\n severity: \"critical\",\n message: \"Error rate exceeds 1%\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"5 minutes\",\n },\n {\n name: \"HighLatency\",\n condition: metrics.latencyP99().pipe(Effect.map((p99) => p99 > 2)),\n severity: \"warning\",\n message: \"P99 latency exceeds 2 seconds\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"10 minutes\",\n },\n {\n name: \"LowAvailability\",\n condition: metrics.availability().pipe(Effect.map((avail) => avail < 99.9)),\n severity: \"critical\",\n message: \"Availability below 99.9% SLO\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"5 minutes\",\n },\n {\n name: \"ErrorBudgetLow\",\n condition: Effect.succeed(false), // Implement based on error budget calc\n severity: \"warning\",\n message: \"Error budget below 25%\",\n labels: { team: \"backend\", service: \"api\" },\n forDuration: \"0 seconds\",\n },\n]\n\n// ============================================\n// 3. Alert manager\n// ============================================\n\ninterface AlertState {\n readonly firing: Map\n readonly resolved: Alert[]\n}\n\nconst makeAlertManager = Effect.gen(function* () {\n const state = yield* Ref.make({\n firing: new Map(),\n resolved: [],\n })\n\n const checkRule = (rule: AlertRule) =>\n Effect.gen(function* () {\n const isTriggered = yield* rule.condition\n\n yield* Ref.modify(state, (s) => {\n const firing = new Map(s.firing)\n const resolved = [...s.resolved]\n const key = rule.name\n\n if (isTriggered) {\n if (!firing.has(key)) {\n // New alert\n firing.set(key, {\n since: new Date(),\n alert: {\n name: rule.name,\n severity: rule.severity,\n message: rule.message,\n timestamp: new Date(),\n labels: rule.labels,\n },\n })\n }\n } else {\n if (firing.has(key)) {\n // Alert resolved\n const prev = firing.get(key)!\n resolved.push({\n ...prev.alert,\n message: `[RESOLVED] ${prev.alert.message}`,\n timestamp: new Date(),\n })\n firing.delete(key)\n }\n }\n\n return [undefined, { firing, resolved }]\n })\n })\n\n const getActiveAlerts = () =>\n Ref.get(state).pipe(\n Effect.map((s) => Array.from(s.firing.values()).map((f) => f.alert))\n )\n\n const getRecentResolved = () =>\n Ref.get(state).pipe(Effect.map((s) => s.resolved.slice(-10)))\n\n return {\n checkRule,\n getActiveAlerts,\n getRecentResolved,\n }\n})\n\n// ============================================\n// 4. Alert notification\n// ============================================\n\ninterface NotificationChannel {\n readonly send: (alert: Alert) => Effect.Effect\n}\n\nconst slackChannel: NotificationChannel = {\n send: (alert) =>\n Effect.gen(function* () {\n const emoji =\n alert.severity === \"critical\"\n ? \"🔴\"\n : alert.severity === \"warning\"\n ? \"🟡\"\n : \"🔵\"\n\n yield* Effect.log(`${emoji} [${alert.severity.toUpperCase()}] ${alert.name}`).pipe(\n Effect.annotateLogs({\n message: alert.message,\n labels: JSON.stringify(alert.labels),\n })\n )\n\n // In real implementation: call Slack API\n }),\n}\n\nconst pagerDutyChannel: NotificationChannel = {\n send: (alert) =>\n Effect.gen(function* () {\n if (alert.severity === \"critical\") {\n yield* Effect.log(\"PagerDuty: Creating incident\").pipe(\n Effect.annotateLogs({ alert: alert.name })\n )\n // In real implementation: call PagerDuty API\n }\n }),\n}\n\n// ============================================\n// 5. Alert evaluation loop\n// ============================================\n\nconst runAlertEvaluation = (\n rules: AlertRule[],\n channels: NotificationChannel[],\n interval: Duration.DurationInput\n) =>\n Effect.gen(function* () {\n const alertManager = yield* makeAlertManager\n const previousAlerts = yield* Ref.make(new Set())\n\n yield* Effect.forever(\n Effect.gen(function* () {\n // Check all rules\n for (const rule of rules) {\n yield* alertManager.checkRule(rule)\n }\n\n // Get current active alerts\n const active = yield* alertManager.getActiveAlerts()\n const current = new Set(active.map((a) => a.name))\n const previous = yield* Ref.get(previousAlerts)\n\n // Find newly firing alerts\n for (const alert of active) {\n if (!previous.has(alert.name)) {\n // New alert - send notifications\n for (const channel of channels) {\n yield* channel.send(alert)\n }\n }\n }\n\n yield* Ref.set(previousAlerts, current)\n yield* Effect.sleep(interval)\n })\n )\n })\n\n// ============================================\n// 6. Prometheus alerting rules (YAML)\n// ============================================\n\nconst prometheusAlertRules = `\ngroups:\n - name: effect-app-alerts\n rules:\n - alert: HighErrorRate\n expr: |\n sum(rate(http_errors_total[5m]))\n /\n sum(rate(http_requests_total[5m]))\n > 0.01\n for: 5m\n labels:\n severity: critical\n annotations:\n summary: \"High error rate detected\"\n description: \"Error rate is {{ $value | humanizePercentage }}\"\n\n - alert: HighLatency\n expr: |\n histogram_quantile(0.99,\n sum(rate(http_request_duration_seconds_bucket[5m])) by (le)\n ) > 2\n for: 10m\n labels:\n severity: warning\n annotations:\n summary: \"High P99 latency\"\n description: \"P99 latency is {{ $value }}s\"\n\n - alert: SLOViolation\n expr: |\n sum(rate(http_requests_total{status!~\"5..\"}[30m]))\n /\n sum(rate(http_requests_total[30m]))\n < 0.999\n for: 5m\n labels:\n severity: critical\n annotations:\n summary: \"SLO violation\"\n description: \"Availability is {{ $value | humanizePercentage }}\"\n`\n```\n\n## Alert Severity Guidelines\n\n| Severity | Response | Example |\n|----------|----------|---------|\n| **Critical** | Page immediately | SLO violation, data loss |\n| **Warning** | Investigate soon | Elevated errors, slow queries |\n| **Info** | Review daily | Capacity trending |\n\n## Good vs Bad Alerts\n\n| ❌ Bad Alert | ✅ Good Alert |\n|-------------|--------------|\n| CPU > 80% | Error rate > 1% |\n| Memory > 90% | P99 latency > 2s |\n| Disk > 85% | Availability < 99.9% |\n| Process restarted | Error budget < 25% |\n\n## Best Practices\n\n1. **Alert on symptoms** - User impact, not causes\n2. **Use for-duration** - Avoid flapping\n3. **Include runbook** - How to respond\n4. **Route by severity** - Critical → page, warning → ticket\n5. **Review regularly** - Remove noisy alerts" - }, - { - "id": "tooling-ci-cd", - "title": "Set Up CI/CD for Effect Projects", - "description": "Use GitHub Actions with proper caching for fast Effect project CI/CD.", - "skillLevel": "intermediate", - "useCases": [ - "tooling-and-debugging" - ], - "example": "### 1. Basic GitHub Actions Workflow\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n strategy:\n matrix:\n node-version: [20.x, 22.x]\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n with:\n bun-version: latest\n\n - name: Install dependencies\n run: bun install\n\n - name: Type check\n run: bun run typecheck\n\n - name: Lint\n run: bun run lint\n\n - name: Test\n run: bun run test\n\n - name: Build\n run: bun run build\n```\n\n### 2. With Caching\n\n```yaml\n# .github/workflows/ci-cached.yml\nname: CI (Cached)\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n with:\n bun-version: latest\n\n # Cache Bun dependencies\n - name: Cache dependencies\n uses: actions/cache@v4\n with:\n path: |\n ~/.bun/install/cache\n node_modules\n key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}\n restore-keys: |\n ${{ runner.os }}-bun-\n\n - name: Install dependencies\n run: bun install\n\n # Cache TypeScript build info\n - name: Cache TypeScript\n uses: actions/cache@v4\n with:\n path: |\n .tsbuildinfo\n dist\n key: ${{ runner.os }}-tsc-${{ hashFiles('**/tsconfig.json', 'src/**/*.ts') }}\n restore-keys: |\n ${{ runner.os }}-tsc-\n\n - name: Type check\n run: bun run typecheck\n\n - name: Lint\n run: bun run lint\n\n - name: Test\n run: bun run test --coverage\n\n - name: Upload coverage\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage/lcov.info\n```\n\n### 3. Package.json Scripts\n\n```json\n{\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --apply .\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\",\n \"build\": \"tsc\",\n \"clean\": \"rm -rf dist .tsbuildinfo\"\n }\n}\n```\n\n### 4. Multi-Stage Workflow\n\n```yaml\n# .github/workflows/ci-full.yml\nname: CI Full\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run lint\n\n typecheck:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run typecheck\n\n test:\n runs-on: ubuntu-latest\n needs: [lint, typecheck]\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run test\n\n build:\n runs-on: ubuntu-latest\n needs: [test]\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run build\n - uses: actions/upload-artifact@v4\n with:\n name: dist\n path: dist/\n\n deploy:\n runs-on: ubuntu-latest\n needs: [build]\n if: github.ref == 'refs/heads/main'\n steps:\n - uses: actions/download-artifact@v4\n with:\n name: dist\n # Add deployment steps\n```\n\n### 5. Release Workflow\n\n```yaml\n# .github/workflows/release.yml\nname: Release\n\non:\n push:\n tags:\n - 'v*'\n\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: oven-sh/setup-bun@v1\n\n - run: bun install\n - run: bun run build\n - run: bun run test\n\n - name: Create Release\n uses: softprops/action-gh-release@v1\n with:\n files: dist/*\n generate_release_notes: true\n```", - "antiPattern": "", - "explanation": "CI/CD for Effect projects ensures:\n\n1. **Type safety** - Catch type errors before merge\n2. **Test coverage** - Run tests automatically\n3. **Consistent builds** - Same environment every time\n4. **Fast feedback** - Know quickly if something broke\n\n---", - "content": "## Guideline\n\nSet up CI/CD with type checking, testing, and optional deployment stages optimized for Effect projects.\n\n---\n\n## Rationale\n\nCI/CD for Effect projects ensures:\n\n1. **Type safety** - Catch type errors before merge\n2. **Test coverage** - Run tests automatically\n3. **Consistent builds** - Same environment every time\n4. **Fast feedback** - Know quickly if something broke\n\n---\n\n## Good Example\n\n### 1. Basic GitHub Actions Workflow\n\n```yaml\n# .github/workflows/ci.yml\nname: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n strategy:\n matrix:\n node-version: [20.x, 22.x]\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n with:\n bun-version: latest\n\n - name: Install dependencies\n run: bun install\n\n - name: Type check\n run: bun run typecheck\n\n - name: Lint\n run: bun run lint\n\n - name: Test\n run: bun run test\n\n - name: Build\n run: bun run build\n```\n\n### 2. With Caching\n\n```yaml\n# .github/workflows/ci-cached.yml\nname: CI (Cached)\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Bun\n uses: oven-sh/setup-bun@v1\n with:\n bun-version: latest\n\n # Cache Bun dependencies\n - name: Cache dependencies\n uses: actions/cache@v4\n with:\n path: |\n ~/.bun/install/cache\n node_modules\n key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}\n restore-keys: |\n ${{ runner.os }}-bun-\n\n - name: Install dependencies\n run: bun install\n\n # Cache TypeScript build info\n - name: Cache TypeScript\n uses: actions/cache@v4\n with:\n path: |\n .tsbuildinfo\n dist\n key: ${{ runner.os }}-tsc-${{ hashFiles('**/tsconfig.json', 'src/**/*.ts') }}\n restore-keys: |\n ${{ runner.os }}-tsc-\n\n - name: Type check\n run: bun run typecheck\n\n - name: Lint\n run: bun run lint\n\n - name: Test\n run: bun run test --coverage\n\n - name: Upload coverage\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage/lcov.info\n```\n\n### 3. Package.json Scripts\n\n```json\n{\n \"scripts\": {\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --apply .\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\",\n \"build\": \"tsc\",\n \"clean\": \"rm -rf dist .tsbuildinfo\"\n }\n}\n```\n\n### 4. Multi-Stage Workflow\n\n```yaml\n# .github/workflows/ci-full.yml\nname: CI Full\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n lint:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run lint\n\n typecheck:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run typecheck\n\n test:\n runs-on: ubuntu-latest\n needs: [lint, typecheck]\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run test\n\n build:\n runs-on: ubuntu-latest\n needs: [test]\n steps:\n - uses: actions/checkout@v4\n - uses: oven-sh/setup-bun@v1\n - run: bun install\n - run: bun run build\n - uses: actions/upload-artifact@v4\n with:\n name: dist\n path: dist/\n\n deploy:\n runs-on: ubuntu-latest\n needs: [build]\n if: github.ref == 'refs/heads/main'\n steps:\n - uses: actions/download-artifact@v4\n with:\n name: dist\n # Add deployment steps\n```\n\n### 5. Release Workflow\n\n```yaml\n# .github/workflows/release.yml\nname: Release\n\non:\n push:\n tags:\n - 'v*'\n\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - uses: oven-sh/setup-bun@v1\n\n - run: bun install\n - run: bun run build\n - run: bun run test\n\n - name: Create Release\n uses: softprops/action-gh-release@v1\n with:\n files: dist/*\n generate_release_notes: true\n```\n\n## Workflow Stages\n\n```\n┌─────────┐ ┌───────────┐ ┌──────┐ ┌───────┐ ┌────────┐\n│ Lint │───►│ Typecheck │───►│ Test │───►│ Build │───►│ Deploy │\n└─────────┘ └───────────┘ └──────┘ └───────┘ └────────┘\n │ │ │ │\n └──────────────┴──────────────┴────────────┘\n Parallel where possible\n```\n\n## Best Practices\n\n1. **Run in parallel** - Lint and typecheck don't depend on each other\n2. **Cache aggressively** - Dependencies and build artifacts\n3. **Fail fast** - Run quick checks first\n4. **Use matrix** - Test multiple Node versions\n5. **Separate deploy** - Only on main branch" - }, - { - "id": "tooling-hello-world", - "title": "Set Up Your Effect Development Environment", - "description": "Install the Effect extension and configure TypeScript for optimal Effect development.", - "skillLevel": "beginner", - "useCases": [ - "tooling-and-debugging" - ], - "example": "", - "antiPattern": "", - "explanation": "A well-configured environment helps you:\n\n1. **See types clearly** - Effect types can be complex\n2. **Get better autocomplete** - Know what methods are available\n3. **Catch errors early** - TypeScript finds problems\n4. **Navigate easily** - Go to definitions, find references\n\n---", - "content": "## Guideline\n\nSet up your development environment with the Effect extension and proper TypeScript configuration for the best experience.\n\n---\n\n## Rationale\n\nA well-configured environment helps you:\n\n1. **See types clearly** - Effect types can be complex\n2. **Get better autocomplete** - Know what methods are available\n3. **Catch errors early** - TypeScript finds problems\n4. **Navigate easily** - Go to definitions, find references\n\n---\n\n## Setup Steps\n\n### 1. Install Effect\n\n```bash\n# Using Bun (recommended)\nbun add effect\n\n# Or npm\nnpm install effect\n\n# Or pnpm\npnpm add effect\n```\n\n### 2. Configure TypeScript\n\nCreate or update `tsconfig.json`:\n\n```json\n{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"strict\": true,\n \"exactOptionalPropertyTypes\": true,\n \"noUncheckedIndexedAccess\": true,\n \"noImplicitReturns\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"declaration\": true,\n \"sourceMap\": true\n },\n \"include\": [\"src/**/*\"],\n \"exclude\": [\"node_modules\"]\n}\n```\n\n### 3. Install VS Code Extensions\n\nInstall the Effect extension for VS Code:\n\n1. Open VS Code\n2. Go to Extensions (Cmd+Shift+X)\n3. Search for \"Effect\"\n4. Install the official Effect extension\n\nThe extension provides:\n- Better type display (unwraps complex Effect types)\n- Quick fixes and refactorings\n- Inline error previews\n- Go to definition for Effect functions\n\n### 4. VS Code Settings (Optional)\n\nAdd to `.vscode/settings.json` for better Effect experience:\n\n```json\n{\n \"typescript.preferences.includePackageJsonAutoImports\": \"on\",\n \"typescript.inlayHints.functionLikeReturnTypes.enabled\": true,\n \"typescript.inlayHints.parameterTypes.enabled\": true,\n \"editor.formatOnSave\": true\n}\n```\n\n---\n\n## Verify Setup\n\nCreate a test file to verify everything works:\n\n```typescript\n// src/hello.ts\nimport { Effect, Console } from \"effect\"\n\nconst program = Effect.gen(function* () {\n yield* Console.log(\"Effect is working!\")\n return 42\n})\n\n// Hover over 'result' - should show: Effect\nconst result = program\n\n// Run it\nEffect.runPromise(result).then(console.log)\n```\n\nRun it:\n\n```bash\nbun src/hello.ts\n# Output: Effect is working!\n# Output: 42\n```\n\n---\n\n## Key Editor Features\n\n| Feature | How to Use |\n|---------|------------|\n| **Hover for types** | Hover over any variable |\n| **Go to definition** | Cmd+Click on a function |\n| **Find references** | Right-click → Find All References |\n| **Autocomplete** | Type `.` after an Effect |\n| **Quick fix** | Click lightbulb or Cmd+. |\n\n---\n\n## Project Structure\n\nRecommended structure for Effect projects:\n\n```\nmy-project/\n├── src/\n│ ├── index.ts # Entry point\n│ ├── services/ # Effect services\n│ ├── errors/ # Tagged errors\n│ └── utils/ # Helper functions\n├── test/\n│ └── *.test.ts # Tests\n├── package.json\n└── tsconfig.json\n```\n\n---\n\n## Next Steps\n\n1. Try the [Getting Started](../getting-started/getting-started-hello-world.mdx) patterns\n2. Install the Effect LSP for advanced features\n3. Explore the Effect documentation" - }, - { - "id": "sink-pattern-batch-insert-stream-records-into-database", - "title": "Sink Pattern 1: Batch Insert Stream Records into Database", - "description": "Batch stream records before database operations to improve throughput and reduce transaction overhead.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates streaming user records from a paginated API and batching them for efficient database insertion.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk } from \"effect\";\n\ninterface User {\n readonly id: number;\n readonly name: string;\n readonly email: string;\n}\n\ninterface PaginatedResponse {\n readonly users: User[];\n readonly nextPage: number | null;\n}\n\n// Mock API that returns paginated users\nconst fetchUserPage = (\n page: number\n): Effect.Effect =>\n Effect.succeed(\n page < 10\n ? {\n users: Array.from({ length: 50 }, (_, i) => ({\n id: page * 50 + i,\n name: `User ${page * 50 + i}`,\n email: `user${page * 50 + i}@example.com`,\n })),\n nextPage: page + 1,\n }\n : { users: [], nextPage: null }\n ).pipe(Effect.delay(\"10 millis\"));\n\n// Mock database insert that takes a batch of users\nconst insertUserBatch = (\n users: readonly User[]\n): Effect.Effect =>\n Effect.sync(() => {\n console.log(`Inserting batch of ${users.length} users`);\n return users.length;\n }).pipe(Effect.delay(\"50 millis\"));\n\n// Create a stream of users from paginated API\nconst userStream: Stream.Stream = Stream.paginateEffect(\n 0,\n (page) =>\n fetchUserPage(page).pipe(\n Effect.map((response) => [\n Chunk.fromIterable(response.users),\n response.nextPage !== null ? Option.some(response.nextPage) : Option.none(),\n ])\n )\n);\n\n// Sink that batches users and inserts them\nconst batchInsertSink: Sink.Sink = Sink.fold(\n 0,\n (count, chunk: Chunk.Chunk) =>\n Effect.gen(function* () {\n const users = Chunk.toArray(chunk);\n const inserted = yield* insertUserBatch(users);\n return count + inserted;\n }),\n (count) => Effect.succeed(count)\n).pipe(\n // Batch into groups of 100 users\n Sink.withChunking((chunk) =>\n chunk.pipe(\n Chunk.chunksOf(100),\n Stream.fromIterable,\n Stream.runCollect\n )\n )\n);\n\n// Run the stream with batching sink\nconst program = Effect.gen(function* () {\n const totalInserted = yield* userStream.pipe(\n Stream.run(batchInsertSink)\n );\n console.log(`Total users inserted: ${totalInserted}`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates a stream** of users from a paginated API\n2. **Defines a batching sink** that collects users into groups of 100\n3. **Inserts each batch** to the database in a single operation\n4. **Tracks total count** of inserted records\n\nThe batching happens automatically—the sink collects elements until the batch size is reached, then processes the complete batch.\n\n---", - "antiPattern": "", - "explanation": "Inserting records one-by-one is inefficient:\n\n- Each insert is a separate database call (network latency, connection overhead)\n- Each insert may be a separate transaction (ACID overhead)\n- Resource contention and connection pool exhaustion at scale\n\nBatching solves this by:\n\n- Grouping N records into a single bulk insert operation\n- Amortizing database overhead across multiple records\n- Maintaining throughput even under backpressure\n- Enabling efficient transaction semantics for the entire batch\n\nFor example, inserting 10,000 records one-by-one might take 100 seconds. Batching in groups of 100 might take just 2-3 seconds.\n\n---", - "content": "## Guideline\n\nWhen consuming a stream of records to persist in a database, collect them into batches using `Sink` before inserting. This reduces the number of database round-trips and transaction overhead, improving overall throughput significantly.\n\n---\n\n## Rationale\n\nInserting records one-by-one is inefficient:\n\n- Each insert is a separate database call (network latency, connection overhead)\n- Each insert may be a separate transaction (ACID overhead)\n- Resource contention and connection pool exhaustion at scale\n\nBatching solves this by:\n\n- Grouping N records into a single bulk insert operation\n- Amortizing database overhead across multiple records\n- Maintaining throughput even under backpressure\n- Enabling efficient transaction semantics for the entire batch\n\nFor example, inserting 10,000 records one-by-one might take 100 seconds. Batching in groups of 100 might take just 2-3 seconds.\n\n---\n\n## Good Example\n\nThis example demonstrates streaming user records from a paginated API and batching them for efficient database insertion.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk } from \"effect\";\n\ninterface User {\n readonly id: number;\n readonly name: string;\n readonly email: string;\n}\n\ninterface PaginatedResponse {\n readonly users: User[];\n readonly nextPage: number | null;\n}\n\n// Mock API that returns paginated users\nconst fetchUserPage = (\n page: number\n): Effect.Effect =>\n Effect.succeed(\n page < 10\n ? {\n users: Array.from({ length: 50 }, (_, i) => ({\n id: page * 50 + i,\n name: `User ${page * 50 + i}`,\n email: `user${page * 50 + i}@example.com`,\n })),\n nextPage: page + 1,\n }\n : { users: [], nextPage: null }\n ).pipe(Effect.delay(\"10 millis\"));\n\n// Mock database insert that takes a batch of users\nconst insertUserBatch = (\n users: readonly User[]\n): Effect.Effect =>\n Effect.sync(() => {\n console.log(`Inserting batch of ${users.length} users`);\n return users.length;\n }).pipe(Effect.delay(\"50 millis\"));\n\n// Create a stream of users from paginated API\nconst userStream: Stream.Stream = Stream.paginateEffect(\n 0,\n (page) =>\n fetchUserPage(page).pipe(\n Effect.map((response) => [\n Chunk.fromIterable(response.users),\n response.nextPage !== null ? Option.some(response.nextPage) : Option.none(),\n ])\n )\n);\n\n// Sink that batches users and inserts them\nconst batchInsertSink: Sink.Sink = Sink.fold(\n 0,\n (count, chunk: Chunk.Chunk) =>\n Effect.gen(function* () {\n const users = Chunk.toArray(chunk);\n const inserted = yield* insertUserBatch(users);\n return count + inserted;\n }),\n (count) => Effect.succeed(count)\n).pipe(\n // Batch into groups of 100 users\n Sink.withChunking((chunk) =>\n chunk.pipe(\n Chunk.chunksOf(100),\n Stream.fromIterable,\n Stream.runCollect\n )\n )\n);\n\n// Run the stream with batching sink\nconst program = Effect.gen(function* () {\n const totalInserted = yield* userStream.pipe(\n Stream.run(batchInsertSink)\n );\n console.log(`Total users inserted: ${totalInserted}`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Creates a stream** of users from a paginated API\n2. **Defines a batching sink** that collects users into groups of 100\n3. **Inserts each batch** to the database in a single operation\n4. **Tracks total count** of inserted records\n\nThe batching happens automatically—the sink collects elements until the batch size is reached, then processes the complete batch.\n\n---\n\n## Advanced: Configurable Batch Size\n\nYou can make batch size configurable and handle partial batches at stream end:\n\n```typescript\ninterface BatchInsertConfig {\n readonly batchSize: number;\n readonly flushTimeoutMs?: number;\n}\n\nconst createBatchInsertSink = (config: BatchInsertConfig) =>\n Sink.fold(\n { buffer: [] as User[], count: 0 },\n (state, user: User) =>\n Effect.gen(function* () {\n const newBuffer = [...state.buffer, user];\n\n if (newBuffer.length >= config.batchSize) {\n // Batch full, insert immediately\n const inserted = yield* insertUserBatch(newBuffer);\n return { buffer: [], count: state.count + inserted };\n }\n\n // Not full yet, keep buffering\n return { buffer: newBuffer, count: state.count };\n }),\n (state) =>\n Effect.gen(function* () {\n // Flush any remaining buffered records\n if (state.buffer.length > 0) {\n const inserted = yield* insertUserBatch(state.buffer);\n return state.count + inserted;\n }\n return state.count;\n })\n );\n\n// Use with custom batch size\nconst customBatchSink = createBatchInsertSink({ batchSize: 50 });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use batching when:**\n\n- Inserting many records from a high-volume source\n- Database connection/transaction overhead is significant\n- You need to optimize throughput over individual latency\n- Records arrive faster than they can be inserted individually\n\n⚠️ **Trade-offs:**\n\n- Increased latency for individual records (buffered until batch fills)\n- Slightly increased memory usage (holding batch in memory)\n- More complex error handling (partial batch failures)\n\n---\n\n## See Also\n\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals\n- [Stream Elements from Paginated API](./stream-from-paginated-api.mdx) - Creating paginated streams\n- [Handle Errors with catchTag](./handle-errors-with-catch.mdx) - Error recovery in streams" - }, - { - "id": "sink-pattern-write-stream-events-to-event-log", - "title": "Sink Pattern 2: Write Stream Events to Event Log", - "description": "Append stream events to an event log with metadata to maintain a complete, ordered record of what happened.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates an event sourcing pattern where a user account stream of events is appended to an event log with metadata.\n\n```typescript\nimport { Effect, Stream, Sink, DateTime, Data } from \"effect\";\n\n// Event types\ntype AccountEvent =\n | AccountCreated\n | MoneyDeposited\n | MoneyWithdrawn\n | AccountClosed;\n\nclass AccountCreated extends Data.TaggedError(\"AccountCreated\")<{\n readonly accountId: string;\n readonly owner: string;\n readonly initialBalance: number;\n}> {}\n\nclass MoneyDeposited extends Data.TaggedError(\"MoneyDeposited\")<{\n readonly accountId: string;\n readonly amount: number;\n}> {}\n\nclass MoneyWithdrawn extends Data.TaggedError(\"MoneyWithdrawn\")<{\n readonly accountId: string;\n readonly amount: number;\n}> {}\n\nclass AccountClosed extends Data.TaggedError(\"AccountClosed\")<{\n readonly accountId: string;\n}> {}\n\n// Event envelope with metadata\ninterface StoredEvent {\n readonly eventId: string; // Unique identifier per event\n readonly eventType: string; // Type of event\n readonly aggregateId: string; // What this event is about\n readonly aggregateType: string; // What kind of thing (Account)\n readonly data: any; // Event payload\n readonly metadata: {\n readonly timestamp: number;\n readonly version: number; // Position in log\n readonly causationId?: string; // What caused this\n };\n}\n\n// Mock event log that appends events\nconst eventLog: StoredEvent[] = [];\nlet eventVersion = 0;\n\nconst appendToEventLog = (\n event: AccountEvent,\n aggregateId: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const storedEvent: StoredEvent = {\n eventId: `evt-${eventVersion}-${Date.now()}`,\n eventType: event._tag,\n aggregateId,\n aggregateType: \"Account\",\n data: event,\n metadata: {\n timestamp: now.toEpochMillis(),\n version: ++eventVersion,\n },\n };\n\n // Append to log (simulated)\n eventLog.push(storedEvent);\n console.log(\n `[v${storedEvent.metadata.version}] ${storedEvent.eventType}: ${aggregateId}`\n );\n\n return storedEvent;\n });\n\n// Simulate a stream of events from various account operations\nconst accountEvents: Stream.Stream<[string, AccountEvent]> = Stream.fromIterable([\n [\n \"acc-1\",\n new AccountCreated({\n accountId: \"acc-1\",\n owner: \"Alice\",\n initialBalance: 1000,\n }),\n ],\n [\"acc-1\", new MoneyDeposited({ accountId: \"acc-1\", amount: 500 })],\n [\"acc-1\", new MoneyWithdrawn({ accountId: \"acc-1\", amount: 200 })],\n [\n \"acc-2\",\n new AccountCreated({\n accountId: \"acc-2\",\n owner: \"Bob\",\n initialBalance: 2000,\n }),\n ],\n [\"acc-2\", new MoneyDeposited({ accountId: \"acc-2\", amount: 1000 })],\n [\"acc-1\", new AccountClosed({ accountId: \"acc-1\" })],\n]);\n\n// Sink that appends each event to the log\nconst eventLogSink: Sink.Sink = Sink.fold(\n 0,\n (count, [aggregateId, event]) =>\n appendToEventLog(event, aggregateId).pipe(\n Effect.map(() => count + 1)\n ),\n (count) => Effect.succeed(count)\n);\n\n// Run the stream and append all events\nconst program = Effect.gen(function* () {\n const totalEvents = yield* accountEvents.pipe(Stream.run(eventLogSink));\n\n console.log(`\\nTotal events appended: ${totalEvents}`);\n console.log(`\\nEvent log contents:`);\n eventLog.forEach((event) => {\n console.log(` [v${event.metadata.version}] ${event.eventType}`);\n });\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Defines event types** using tagged errors (AccountCreated, MoneyDeposited, etc.)\n2. **Creates event envelopes** with metadata (timestamp, version, causation)\n3. **Streams events** from various sources\n4. **Appends to log** with proper versioning and ordering\n5. **Maintains history** for reconstruction and audit\n\n---", - "antiPattern": "", - "explanation": "Event logs are foundational to many patterns:\n\n- **Event Sourcing**: Instead of storing current state, store the sequence of events that led to it\n- **Audit Trails**: Complete, tamper-proof record of who did what and when\n- **Temporal Queries**: Reconstruct state at any point in time\n- **Consistency**: Single source of truth for what happened\n- **Replay**: Rebuild state or test changes by replaying events\n\nUnlike batch inserts which are transactional, event logs are append-only. Each event is immutable once written. This simplicity enables:\n\n- Fast appends (no updates, just sequential writes)\n- Natural ordering (events in write order)\n- Easy distribution (replicate the log)\n- Strong consistency (events are facts that don't change)\n\n---", - "content": "## Guideline\n\nWhen consuming a stream of events that represent changes in your system, append each event to an event log using `Sink`. Event logs provide immutable, ordered records that enable event sourcing, audit trails, and temporal queries.\n\n---\n\n## Rationale\n\nEvent logs are foundational to many patterns:\n\n- **Event Sourcing**: Instead of storing current state, store the sequence of events that led to it\n- **Audit Trails**: Complete, tamper-proof record of who did what and when\n- **Temporal Queries**: Reconstruct state at any point in time\n- **Consistency**: Single source of truth for what happened\n- **Replay**: Rebuild state or test changes by replaying events\n\nUnlike batch inserts which are transactional, event logs are append-only. Each event is immutable once written. This simplicity enables:\n\n- Fast appends (no updates, just sequential writes)\n- Natural ordering (events in write order)\n- Easy distribution (replicate the log)\n- Strong consistency (events are facts that don't change)\n\n---\n\n## Good Example\n\nThis example demonstrates an event sourcing pattern where a user account stream of events is appended to an event log with metadata.\n\n```typescript\nimport { Effect, Stream, Sink, DateTime, Data } from \"effect\";\n\n// Event types\ntype AccountEvent =\n | AccountCreated\n | MoneyDeposited\n | MoneyWithdrawn\n | AccountClosed;\n\nclass AccountCreated extends Data.TaggedError(\"AccountCreated\")<{\n readonly accountId: string;\n readonly owner: string;\n readonly initialBalance: number;\n}> {}\n\nclass MoneyDeposited extends Data.TaggedError(\"MoneyDeposited\")<{\n readonly accountId: string;\n readonly amount: number;\n}> {}\n\nclass MoneyWithdrawn extends Data.TaggedError(\"MoneyWithdrawn\")<{\n readonly accountId: string;\n readonly amount: number;\n}> {}\n\nclass AccountClosed extends Data.TaggedError(\"AccountClosed\")<{\n readonly accountId: string;\n}> {}\n\n// Event envelope with metadata\ninterface StoredEvent {\n readonly eventId: string; // Unique identifier per event\n readonly eventType: string; // Type of event\n readonly aggregateId: string; // What this event is about\n readonly aggregateType: string; // What kind of thing (Account)\n readonly data: any; // Event payload\n readonly metadata: {\n readonly timestamp: number;\n readonly version: number; // Position in log\n readonly causationId?: string; // What caused this\n };\n}\n\n// Mock event log that appends events\nconst eventLog: StoredEvent[] = [];\nlet eventVersion = 0;\n\nconst appendToEventLog = (\n event: AccountEvent,\n aggregateId: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const storedEvent: StoredEvent = {\n eventId: `evt-${eventVersion}-${Date.now()}`,\n eventType: event._tag,\n aggregateId,\n aggregateType: \"Account\",\n data: event,\n metadata: {\n timestamp: now.toEpochMillis(),\n version: ++eventVersion,\n },\n };\n\n // Append to log (simulated)\n eventLog.push(storedEvent);\n console.log(\n `[v${storedEvent.metadata.version}] ${storedEvent.eventType}: ${aggregateId}`\n );\n\n return storedEvent;\n });\n\n// Simulate a stream of events from various account operations\nconst accountEvents: Stream.Stream<[string, AccountEvent]> = Stream.fromIterable([\n [\n \"acc-1\",\n new AccountCreated({\n accountId: \"acc-1\",\n owner: \"Alice\",\n initialBalance: 1000,\n }),\n ],\n [\"acc-1\", new MoneyDeposited({ accountId: \"acc-1\", amount: 500 })],\n [\"acc-1\", new MoneyWithdrawn({ accountId: \"acc-1\", amount: 200 })],\n [\n \"acc-2\",\n new AccountCreated({\n accountId: \"acc-2\",\n owner: \"Bob\",\n initialBalance: 2000,\n }),\n ],\n [\"acc-2\", new MoneyDeposited({ accountId: \"acc-2\", amount: 1000 })],\n [\"acc-1\", new AccountClosed({ accountId: \"acc-1\" })],\n]);\n\n// Sink that appends each event to the log\nconst eventLogSink: Sink.Sink = Sink.fold(\n 0,\n (count, [aggregateId, event]) =>\n appendToEventLog(event, aggregateId).pipe(\n Effect.map(() => count + 1)\n ),\n (count) => Effect.succeed(count)\n);\n\n// Run the stream and append all events\nconst program = Effect.gen(function* () {\n const totalEvents = yield* accountEvents.pipe(Stream.run(eventLogSink));\n\n console.log(`\\nTotal events appended: ${totalEvents}`);\n console.log(`\\nEvent log contents:`);\n eventLog.forEach((event) => {\n console.log(` [v${event.metadata.version}] ${event.eventType}`);\n });\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Defines event types** using tagged errors (AccountCreated, MoneyDeposited, etc.)\n2. **Creates event envelopes** with metadata (timestamp, version, causation)\n3. **Streams events** from various sources\n4. **Appends to log** with proper versioning and ordering\n5. **Maintains history** for reconstruction and audit\n\n---\n\n## Advanced: Event Causation Tracking\n\nTrack which events caused other events (for distributed systems):\n\n```typescript\ninterface EnhancedStoredEvent extends StoredEvent {\n readonly metadata: StoredEvent[\"metadata\"] & {\n readonly causationId?: string; // Event that caused this one\n readonly correlationId?: string; // Part of same logical operation\n };\n}\n\nconst appendWithCausation = (\n event: AccountEvent,\n aggregateId: string,\n causationId?: string\n): Effect.Effect =>\n Effect.gen(function* () {\n const now = yield* DateTime.now;\n const storedEvent: EnhancedStoredEvent = {\n eventId: `evt-${eventVersion}-${Date.now()}`,\n eventType: event._tag,\n aggregateId,\n aggregateType: \"Account\",\n data: event,\n metadata: {\n timestamp: now.toEpochMillis(),\n version: ++eventVersion,\n causationId, // Link to causing event\n },\n };\n\n eventLog.push(storedEvent);\n return storedEvent;\n });\n```\n\n---\n\n## Advanced: Reconstructing State from Events\n\nRead events back from the log to reconstruct current state:\n\n```typescript\ninterface AccountState {\n readonly accountId: string;\n readonly owner: string;\n readonly balance: number;\n readonly closed: boolean;\n}\n\nconst reconstructAccount = (\n accountId: string\n): Effect.Effect =>\n Effect.sync(() => {\n const events = eventLog.filter(\n (e) => e.aggregateId === accountId && e.aggregateType === \"Account\"\n );\n\n if (events.length === 0) return null;\n\n let state: AccountState = {\n accountId,\n owner: \"\",\n balance: 0,\n closed: false,\n };\n\n for (const event of events) {\n if (event.eventType === \"AccountCreated\") {\n const created = event.data as AccountCreated;\n state = {\n ...state,\n owner: created.owner,\n balance: created.initialBalance,\n };\n } else if (event.eventType === \"MoneyDeposited\") {\n const deposited = event.data as MoneyDeposited;\n state = { ...state, balance: state.balance + deposited.amount };\n } else if (event.eventType === \"MoneyWithdrawn\") {\n const withdrawn = event.data as MoneyWithdrawn;\n state = { ...state, balance: state.balance - withdrawn.amount };\n } else if (event.eventType === \"AccountClosed\") {\n state = { ...state, closed: true };\n }\n }\n\n return state;\n });\n\n// Usage\nconst currentState = yield* reconstructAccount(\"acc-1\");\nconsole.log(`Account balance: ${currentState?.balance}`);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use event logs when:**\n\n- You need a complete audit trail\n- You want to replay events to understand what happened\n- You're implementing event sourcing\n- You need temporal queries (\"What was the balance on date X?\")\n- You want strong consistency and traceability\n- Multiple systems need to stay in sync\n\n⚠️ **Trade-offs:**\n\n- State reconstruction requires replaying events (slower than queries)\n- Event log grows indefinitely (storage considerations)\n- Schema changes require event migration strategies\n- More complex than simple CRUD operations\n\n---\n\n## See Also\n\n- [Batch Insert Stream Records into Database](./batch-insert-stream-records-into-database.mdx) - Bulk persistence\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals\n- [Define Tagged Errors](./define-tagged-errors.mdx) - Event type definition" - }, - { - "id": "sink-pattern-write-stream-lines-to-file", - "title": "Sink Pattern 3: Write Stream Lines to File", - "description": "Write streaming lines to a file efficiently using buffered output and proper resource management.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates streaming log entries and writing them to a file with buffering.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, FileSystem } from \"effect\";\n\ninterface LogEntry {\n readonly level: \"debug\" | \"info\" | \"warn\" | \"error\";\n readonly message: string;\n readonly timestamp: number;\n}\n\n// Format a log entry as a line\nconst formatLogLine = (entry: LogEntry): string => {\n const iso = new Date(entry.timestamp).toISOString();\n return `[${iso}] ${entry.level.toUpperCase()}: ${entry.message}`;\n};\n\n// Simulate a stream of log entries\nconst logStream: Stream.Stream = Stream.fromIterable([\n { level: \"info\", message: \"Server starting\", timestamp: Date.now() },\n { level: \"debug\", message: \"Loading config\", timestamp: Date.now() + 100 },\n { level: \"info\", message: \"Connected to database\", timestamp: Date.now() + 200 },\n { level: \"warn\", message: \"High memory usage detected\", timestamp: Date.now() + 300 },\n { level: \"info\", message: \"Processing request\", timestamp: Date.now() + 400 },\n { level: \"error\", message: \"Connection timeout\", timestamp: Date.now() + 500 },\n { level: \"info\", message: \"Retrying connection\", timestamp: Date.now() + 600 },\n { level: \"info\", message: \"Connection restored\", timestamp: Date.now() + 700 },\n]);\n\n// Create a file writer sink with buffering\nconst createFileWriteSink = (\n filePath: string,\n bufferSize: number = 100\n): Sink.Sink =>\n Effect.scoped(\n Effect.gen(function* () {\n // Open file in append mode\n const fs = yield* FileSystem.FileSystem;\n const handle = yield* fs.open(filePath, \"a\");\n\n let buffer: string[] = [];\n let lineCount = 0;\n\n // Flush buffered lines to disk\n const flush = Effect.gen(function* () {\n if (buffer.length === 0) return;\n\n const content = buffer.join(\"\\n\") + \"\\n\";\n yield* fs.write(handle, content);\n buffer = [];\n });\n\n // Return the sink\n return Sink.fold(\n 0,\n (count, line: string) =>\n Effect.gen(function* () {\n buffer.push(line);\n const newCount = count + 1;\n\n // Flush when buffer reaches size limit\n if (buffer.length >= bufferSize) {\n yield* flush;\n }\n\n return newCount;\n }),\n (count) =>\n Effect.gen(function* () {\n // Flush any remaining lines before closing\n yield* flush;\n yield* fs.close(handle);\n return count;\n })\n );\n })\n ).pipe(\n Effect.flatten\n );\n\n// Process the log stream\nconst program = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const filePath = \"/tmp/app.log\";\n\n // Clear the file first\n yield* fs.writeFileString(filePath, \"\");\n\n // Stream logs, format them, and write to file\n const written = yield* logStream.pipe(\n Stream.map(formatLogLine),\n Stream.run(createFileWriteSink(filePath, 50)) // Buffer 50 lines before flush\n );\n\n console.log(`Wrote ${written} log lines to ${filePath}`);\n\n // Read back the file to verify\n const content = yield* fs.readFileString(filePath);\n console.log(\"\\nFile contents:\");\n console.log(content);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Opens a file** for appending\n2. **Buffers log lines** in memory (50 lines before flush)\n3. **Flushes periodically** when buffer fills or stream ends\n4. **Closes the file** safely using scopes\n5. **Tracks line count** for confirmation\n\n---", - "antiPattern": "", - "explanation": "Writing stream data to files requires:\n\n- **Buffering**: Writing one line at a time is slow. Buffer multiple lines before flushing to disk\n- **Efficiency**: Reduce system calls and I/O overhead by batching writes\n- **Resource Management**: Ensure file handles are properly closed even on errors\n- **Ordering**: Maintain the order of lines as they appear in the stream\n\nThis pattern is essential for:\n\n- Log files and audit trails\n- CSV/JSON Line export\n- Streaming data archival\n- Data pipelines with file intermediates\n\n---", - "content": "## Guideline\n\nWhen consuming a stream of data to persist as lines in a file, use `Sink` with a file writer. Buffer the output for efficiency and ensure proper resource cleanup using Effect's scope management.\n\n---\n\n## Rationale\n\nWriting stream data to files requires:\n\n- **Buffering**: Writing one line at a time is slow. Buffer multiple lines before flushing to disk\n- **Efficiency**: Reduce system calls and I/O overhead by batching writes\n- **Resource Management**: Ensure file handles are properly closed even on errors\n- **Ordering**: Maintain the order of lines as they appear in the stream\n\nThis pattern is essential for:\n\n- Log files and audit trails\n- CSV/JSON Line export\n- Streaming data archival\n- Data pipelines with file intermediates\n\n---\n\n## Good Example\n\nThis example demonstrates streaming log entries and writing them to a file with buffering.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, FileSystem } from \"effect\";\n\ninterface LogEntry {\n readonly level: \"debug\" | \"info\" | \"warn\" | \"error\";\n readonly message: string;\n readonly timestamp: number;\n}\n\n// Format a log entry as a line\nconst formatLogLine = (entry: LogEntry): string => {\n const iso = new Date(entry.timestamp).toISOString();\n return `[${iso}] ${entry.level.toUpperCase()}: ${entry.message}`;\n};\n\n// Simulate a stream of log entries\nconst logStream: Stream.Stream = Stream.fromIterable([\n { level: \"info\", message: \"Server starting\", timestamp: Date.now() },\n { level: \"debug\", message: \"Loading config\", timestamp: Date.now() + 100 },\n { level: \"info\", message: \"Connected to database\", timestamp: Date.now() + 200 },\n { level: \"warn\", message: \"High memory usage detected\", timestamp: Date.now() + 300 },\n { level: \"info\", message: \"Processing request\", timestamp: Date.now() + 400 },\n { level: \"error\", message: \"Connection timeout\", timestamp: Date.now() + 500 },\n { level: \"info\", message: \"Retrying connection\", timestamp: Date.now() + 600 },\n { level: \"info\", message: \"Connection restored\", timestamp: Date.now() + 700 },\n]);\n\n// Create a file writer sink with buffering\nconst createFileWriteSink = (\n filePath: string,\n bufferSize: number = 100\n): Sink.Sink =>\n Effect.scoped(\n Effect.gen(function* () {\n // Open file in append mode\n const fs = yield* FileSystem.FileSystem;\n const handle = yield* fs.open(filePath, \"a\");\n\n let buffer: string[] = [];\n let lineCount = 0;\n\n // Flush buffered lines to disk\n const flush = Effect.gen(function* () {\n if (buffer.length === 0) return;\n\n const content = buffer.join(\"\\n\") + \"\\n\";\n yield* fs.write(handle, content);\n buffer = [];\n });\n\n // Return the sink\n return Sink.fold(\n 0,\n (count, line: string) =>\n Effect.gen(function* () {\n buffer.push(line);\n const newCount = count + 1;\n\n // Flush when buffer reaches size limit\n if (buffer.length >= bufferSize) {\n yield* flush;\n }\n\n return newCount;\n }),\n (count) =>\n Effect.gen(function* () {\n // Flush any remaining lines before closing\n yield* flush;\n yield* fs.close(handle);\n return count;\n })\n );\n })\n ).pipe(\n Effect.flatten\n );\n\n// Process the log stream\nconst program = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const filePath = \"/tmp/app.log\";\n\n // Clear the file first\n yield* fs.writeFileString(filePath, \"\");\n\n // Stream logs, format them, and write to file\n const written = yield* logStream.pipe(\n Stream.map(formatLogLine),\n Stream.run(createFileWriteSink(filePath, 50)) // Buffer 50 lines before flush\n );\n\n console.log(`Wrote ${written} log lines to ${filePath}`);\n\n // Read back the file to verify\n const content = yield* fs.readFileString(filePath);\n console.log(\"\\nFile contents:\");\n console.log(content);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Opens a file** for appending\n2. **Buffers log lines** in memory (50 lines before flush)\n3. **Flushes periodically** when buffer fills or stream ends\n4. **Closes the file** safely using scopes\n5. **Tracks line count** for confirmation\n\n---\n\n## Advanced: Rotating Log Files\n\nHandle log rotation when file size or time limit is reached:\n\n```typescript\ninterface RotationPolicy {\n readonly maxLines?: number; // Rotate after N lines\n readonly maxSizeBytes?: number; // Rotate after N bytes\n readonly maxAgeMs?: number; // Rotate after N milliseconds\n}\n\nconst createRotatingFileSink = (\n baseFilePath: string,\n policy: RotationPolicy,\n bufferSize: number = 100\n): Sink.Sink =>\n Effect.scoped(\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n let fileIndex = 0;\n let currentLineCount = 0;\n let currentByteCount = 0;\n\n const getFilePath = () =>\n fileIndex === 0\n ? baseFilePath\n : baseFilePath.replace(/(\\.[^.]*)?$/, `.${fileIndex}$1`);\n\n let handle = yield* fs.open(getFilePath(), \"a\");\n let buffer: string[] = [];\n\n const rotateFile = Effect.gen(function* () {\n // Flush current buffer\n if (buffer.length > 0) {\n const content = buffer.join(\"\\n\") + \"\\n\";\n yield* fs.write(handle, content);\n buffer = [];\n }\n\n // Close current file\n yield* fs.close(handle);\n\n // Open new file\n fileIndex++;\n currentLineCount = 0;\n currentByteCount = 0;\n handle = yield* fs.open(getFilePath(), \"a\");\n console.log(`Rotated to ${getFilePath()}`);\n });\n\n const shouldRotate = (line: string): boolean => {\n if (policy.maxLines && currentLineCount >= policy.maxLines)\n return true;\n if (policy.maxSizeBytes && currentByteCount >= policy.maxSizeBytes)\n return true;\n return false;\n };\n\n return Sink.fold(\n 0,\n (count, line: string) =>\n Effect.gen(function* () {\n if (shouldRotate(line)) {\n yield* rotateFile;\n }\n\n buffer.push(line);\n currentLineCount++;\n currentByteCount += line.length + 1; // +1 for newline\n\n if (buffer.length >= bufferSize) {\n const content = buffer.join(\"\\n\") + \"\\n\";\n yield* fs.write(handle, content);\n buffer = [];\n }\n\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n if (buffer.length > 0) {\n const content = buffer.join(\"\\n\") + \"\\n\";\n yield* fs.write(handle, content);\n }\n yield* fs.close(handle);\n return count;\n })\n );\n })\n ).pipe(Effect.flatten);\n```\n\n---\n\n## Advanced: Compression and Archival\n\nCompress rotated files automatically:\n\n```typescript\nimport { execSync } from \"child_process\";\n\nconst compressRotatedFile = (filePath: string): Effect.Effect =>\n Effect.try(() => {\n execSync(`gzip -f ${filePath}`);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.log(`Failed to compress ${filePath}: ${error}`)\n )\n );\n\n// In the rotation logic:\nconst rotateFile = Effect.gen(function* () {\n // ... existing rotation code ...\n\n // Compress the old file\n const oldFilePath = getFilePath();\n yield* compressRotatedFile(oldFilePath);\n});\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use file writing when:**\n\n- Streaming logs or audit trails\n- Exporting large datasets to files\n- Creating line-oriented formats (JSON Lines, CSV)\n- Need persistent local storage\n- Data volume too large to fit in memory\n\n⚠️ **Trade-offs:**\n\n- Disk I/O can be slower than in-memory processing\n- File system limitations on file size and path length\n- Requires cleanup of old/rotated files\n- Less efficient than databases for querying\n\n---\n\n## See Also\n\n- [Sink Pattern 1: Batch Insert](./sink-pattern-batch-insert-stream-records-into-database.mdx) - Bulk database persistence\n- [Sink Pattern 2: Event Log](./sink-pattern-write-stream-events-to-event-log.mdx) - Event sourcing\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals\n- [Manage Resource Lifecycles with Scope](./manage-resource-lifecycles-with-scope.mdx) - File handle cleanup" - }, - { - "id": "sink-pattern-send-stream-records-to-message-queue", - "title": "Sink Pattern 4: Send Stream Records to Message Queue", - "description": "Stream records to message queues with proper batching and acknowledgment for reliable distributed data flow.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates streaming sensor readings and publishing them to a message queue with topic-based partitioning.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk } from \"effect\";\n\ninterface SensorReading {\n readonly sensorId: string;\n readonly location: string;\n readonly temperature: number;\n readonly humidity: number;\n readonly timestamp: number;\n}\n\n// Mock message queue publisher\ninterface QueuePublisher {\n readonly publish: (\n topic: string,\n partition: string,\n messages: readonly SensorReading[]\n ) => Effect.Effect<{ acknowledged: number; messageIds: string[] }>;\n}\n\n// Create a mock queue publisher\nconst createMockPublisher = (): QueuePublisher => {\n const publishedMessages: Record = {};\n\n return {\n publish: (topic, partition, messages) =>\n Effect.gen(function* () {\n const key = `${topic}/${partition}`;\n publishedMessages[key] = [\n ...(publishedMessages[key] ?? []),\n ...messages,\n ];\n\n const messageIds = Array.from({ length: messages.length }, (_, i) =>\n `msg-${Date.now()}-${i}`\n );\n\n console.log(\n `Published ${messages.length} messages to ${key} (batch)`\n );\n\n return { acknowledged: messages.length, messageIds };\n }),\n };\n};\n\n// Determine the partition key based on sensor location\nconst getPartitionKey = (reading: SensorReading): string =>\n reading.location; // Route by location for data locality\n\n// Simulate a stream of sensor readings\nconst sensorStream: Stream.Stream = Stream.fromIterable([\n {\n sensorId: \"temp-1\",\n location: \"warehouse-a\",\n temperature: 22.5,\n humidity: 45,\n timestamp: Date.now(),\n },\n {\n sensorId: \"temp-2\",\n location: \"warehouse-b\",\n temperature: 21.0,\n humidity: 50,\n timestamp: Date.now() + 100,\n },\n {\n sensorId: \"temp-3\",\n location: \"warehouse-a\",\n temperature: 22.8,\n humidity: 46,\n timestamp: Date.now() + 200,\n },\n {\n sensorId: \"temp-4\",\n location: \"warehouse-c\",\n temperature: 20.5,\n humidity: 55,\n timestamp: Date.now() + 300,\n },\n {\n sensorId: \"temp-5\",\n location: \"warehouse-b\",\n temperature: 21.2,\n humidity: 51,\n timestamp: Date.now() + 400,\n },\n {\n sensorId: \"temp-6\",\n location: \"warehouse-a\",\n temperature: 23.0,\n humidity: 47,\n timestamp: Date.now() + 500,\n },\n]);\n\n// Create a sink that batches and publishes to message queue\nconst createQueuePublishSink = (\n publisher: QueuePublisher,\n topic: string,\n batchSize: number = 100\n): Sink.Sink =>\n Sink.fold(\n { batches: new Map(), totalPublished: 0 },\n (state, reading) =>\n Effect.gen(function* () {\n const partition = getPartitionKey(reading);\n const batch = state.batches.get(partition) ?? [];\n const newBatch = [...batch, reading];\n\n if (newBatch.length >= batchSize) {\n // Batch is full, publish it\n const result = yield* publisher.publish(topic, partition, newBatch);\n const newState = new Map(state.batches);\n newState.delete(partition);\n\n return {\n ...state,\n batches: newState,\n totalPublished: state.totalPublished + result.acknowledged,\n };\n } else {\n // Add to batch and continue\n const newState = new Map(state.batches);\n newState.set(partition, newBatch);\n\n return { ...state, batches: newState };\n }\n }),\n (state) =>\n Effect.gen(function* () {\n let finalCount = state.totalPublished;\n\n // Publish any remaining partial batches\n for (const [partition, batch] of state.batches) {\n if (batch.length > 0) {\n const result = yield* publisher.publish(topic, partition, batch);\n finalCount += result.acknowledged;\n }\n }\n\n return finalCount;\n })\n );\n\n// Run the stream and publish to queue\nconst program = Effect.gen(function* () {\n const publisher = createMockPublisher();\n const topic = \"sensor-readings\";\n\n const published = yield* sensorStream.pipe(\n Stream.run(createQueuePublishSink(publisher, topic, 50)) // Batch size of 50\n );\n\n console.log(\n `\\nTotal messages published to queue: ${published}`\n );\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Groups readings by partition** (location) for data locality\n2. **Batches records** before publishing (50 at a time)\n3. **Publishes batches** to the queue with partition key\n4. **Flushes partial batches** when stream ends\n5. **Tracks acknowledgments** from the queue\n\n---", - "antiPattern": "", - "explanation": "Message queues are the backbone of event-driven architectures:\n\n- **Decoupling**: Producers don't wait for consumers\n- **Scalability**: Multiple subscribers can consume independently\n- **Durability**: Messages persist even if subscribers are down\n- **Ordering**: Maintain event sequence (per partition/topic)\n- **Reliability**: Acknowledgments and retries ensure no message loss\n\nUnlike direct writes which block, queue publishing is asynchronous and enables:\n\n- High-throughput publishing (batch multiple records per operation)\n- Backpressure handling (queue manages flow)\n- Multi-subscriber patterns (fan-out)\n- Dead letter queues for error handling\n\n---", - "content": "## Guideline\n\nWhen consuming a stream of events that need to be distributed to other systems, use `Sink` to publish them to a message queue. Message queues provide reliable, scalable distribution with guarantees like ordering and exactly-once semantics.\n\n---\n\n## Rationale\n\nMessage queues are the backbone of event-driven architectures:\n\n- **Decoupling**: Producers don't wait for consumers\n- **Scalability**: Multiple subscribers can consume independently\n- **Durability**: Messages persist even if subscribers are down\n- **Ordering**: Maintain event sequence (per partition/topic)\n- **Reliability**: Acknowledgments and retries ensure no message loss\n\nUnlike direct writes which block, queue publishing is asynchronous and enables:\n\n- High-throughput publishing (batch multiple records per operation)\n- Backpressure handling (queue manages flow)\n- Multi-subscriber patterns (fan-out)\n- Dead letter queues for error handling\n\n---\n\n## Good Example\n\nThis example demonstrates streaming sensor readings and publishing them to a message queue with topic-based partitioning.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk } from \"effect\";\n\ninterface SensorReading {\n readonly sensorId: string;\n readonly location: string;\n readonly temperature: number;\n readonly humidity: number;\n readonly timestamp: number;\n}\n\n// Mock message queue publisher\ninterface QueuePublisher {\n readonly publish: (\n topic: string,\n partition: string,\n messages: readonly SensorReading[]\n ) => Effect.Effect<{ acknowledged: number; messageIds: string[] }>;\n}\n\n// Create a mock queue publisher\nconst createMockPublisher = (): QueuePublisher => {\n const publishedMessages: Record = {};\n\n return {\n publish: (topic, partition, messages) =>\n Effect.gen(function* () {\n const key = `${topic}/${partition}`;\n publishedMessages[key] = [\n ...(publishedMessages[key] ?? []),\n ...messages,\n ];\n\n const messageIds = Array.from({ length: messages.length }, (_, i) =>\n `msg-${Date.now()}-${i}`\n );\n\n console.log(\n `Published ${messages.length} messages to ${key} (batch)`\n );\n\n return { acknowledged: messages.length, messageIds };\n }),\n };\n};\n\n// Determine the partition key based on sensor location\nconst getPartitionKey = (reading: SensorReading): string =>\n reading.location; // Route by location for data locality\n\n// Simulate a stream of sensor readings\nconst sensorStream: Stream.Stream = Stream.fromIterable([\n {\n sensorId: \"temp-1\",\n location: \"warehouse-a\",\n temperature: 22.5,\n humidity: 45,\n timestamp: Date.now(),\n },\n {\n sensorId: \"temp-2\",\n location: \"warehouse-b\",\n temperature: 21.0,\n humidity: 50,\n timestamp: Date.now() + 100,\n },\n {\n sensorId: \"temp-3\",\n location: \"warehouse-a\",\n temperature: 22.8,\n humidity: 46,\n timestamp: Date.now() + 200,\n },\n {\n sensorId: \"temp-4\",\n location: \"warehouse-c\",\n temperature: 20.5,\n humidity: 55,\n timestamp: Date.now() + 300,\n },\n {\n sensorId: \"temp-5\",\n location: \"warehouse-b\",\n temperature: 21.2,\n humidity: 51,\n timestamp: Date.now() + 400,\n },\n {\n sensorId: \"temp-6\",\n location: \"warehouse-a\",\n temperature: 23.0,\n humidity: 47,\n timestamp: Date.now() + 500,\n },\n]);\n\n// Create a sink that batches and publishes to message queue\nconst createQueuePublishSink = (\n publisher: QueuePublisher,\n topic: string,\n batchSize: number = 100\n): Sink.Sink =>\n Sink.fold(\n { batches: new Map(), totalPublished: 0 },\n (state, reading) =>\n Effect.gen(function* () {\n const partition = getPartitionKey(reading);\n const batch = state.batches.get(partition) ?? [];\n const newBatch = [...batch, reading];\n\n if (newBatch.length >= batchSize) {\n // Batch is full, publish it\n const result = yield* publisher.publish(topic, partition, newBatch);\n const newState = new Map(state.batches);\n newState.delete(partition);\n\n return {\n ...state,\n batches: newState,\n totalPublished: state.totalPublished + result.acknowledged,\n };\n } else {\n // Add to batch and continue\n const newState = new Map(state.batches);\n newState.set(partition, newBatch);\n\n return { ...state, batches: newState };\n }\n }),\n (state) =>\n Effect.gen(function* () {\n let finalCount = state.totalPublished;\n\n // Publish any remaining partial batches\n for (const [partition, batch] of state.batches) {\n if (batch.length > 0) {\n const result = yield* publisher.publish(topic, partition, batch);\n finalCount += result.acknowledged;\n }\n }\n\n return finalCount;\n })\n );\n\n// Run the stream and publish to queue\nconst program = Effect.gen(function* () {\n const publisher = createMockPublisher();\n const topic = \"sensor-readings\";\n\n const published = yield* sensorStream.pipe(\n Stream.run(createQueuePublishSink(publisher, topic, 50)) // Batch size of 50\n );\n\n console.log(\n `\\nTotal messages published to queue: ${published}`\n );\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Groups readings by partition** (location) for data locality\n2. **Batches records** before publishing (50 at a time)\n3. **Publishes batches** to the queue with partition key\n4. **Flushes partial batches** when stream ends\n5. **Tracks acknowledgments** from the queue\n\n---\n\n## Advanced: Topic-Based Routing\n\nRoute different types of records to different topics:\n\n```typescript\ntype StreamRecord = SensorReading | AlertEvent | SystemMetric;\n\ntype Topic = \"sensor-readings\" | \"alerts\" | \"metrics\";\n\nconst getTopicAndPartition = (\n record: StreamRecord\n): { topic: Topic; partition: string } => {\n if (\"temperature\" in record) {\n return {\n topic: \"sensor-readings\",\n partition: record.location,\n };\n } else if (\"alertType\" in record) {\n return {\n topic: \"alerts\",\n partition: record.severity,\n };\n } else {\n return {\n topic: \"metrics\",\n partition: record.metricType,\n };\n }\n};\n\n// Modified sink that routes to multiple topics\nconst createRouterPublishSink = (\n publisher: QueuePublisher,\n batchSize: number = 100\n): Sink.Sink =>\n Sink.fold(\n { batches: new Map(), totalPublished: 0 },\n (state, record) =>\n Effect.gen(function* () {\n const { topic, partition } = getTopicAndPartition(record);\n const batchKey = `${topic}/${partition}`;\n const batch = state.batches.get(batchKey) ?? [];\n const newBatch = [...batch, record];\n\n if (newBatch.length >= batchSize) {\n const result = yield* publisher.publish(topic, partition, newBatch);\n const newState = new Map(state.batches);\n newState.delete(batchKey);\n\n return {\n ...state,\n batches: newState,\n totalPublished: state.totalPublished + result.acknowledged,\n };\n } else {\n const newState = new Map(state.batches);\n newState.set(batchKey, newBatch);\n return { ...state, batches: newState };\n }\n }),\n (state) =>\n Effect.gen(function* () {\n let finalCount = state.totalPublished;\n\n for (const [batchKey, batch] of state.batches) {\n if (batch.length > 0) {\n const [topic, partition] = batchKey.split(\"/\");\n const result = yield* publisher.publish(\n topic as Topic,\n partition,\n batch\n );\n finalCount += result.acknowledged;\n }\n }\n\n return finalCount;\n })\n );\n```\n\n---\n\n## Advanced: Handling Publishing Failures\n\nImplement retry logic and dead letter handling:\n\n```typescript\ninterface PublishConfig {\n readonly batchSize: number;\n readonly maxRetries: number;\n readonly backoffMs: number;\n}\n\nconst createResilientPublishSink = (\n publisher: QueuePublisher,\n topic: string,\n config: PublishConfig\n): Sink.Sink =>\n Sink.fold(\n { batches: new Map(), totalPublished: 0, dead: [] as SensorReading[] },\n (state, reading) =>\n Effect.gen(function* () {\n const partition = getPartitionKey(reading);\n const batch = state.batches.get(partition) ?? [];\n const newBatch = [...batch, reading];\n\n if (newBatch.length >= config.batchSize) {\n // Try to publish with retries\n const result = yield* publisher\n .publish(topic, partition, newBatch)\n .pipe(\n Effect.retry({\n times: config.maxRetries,\n delay: () =>\n Effect.sleep(`${config.backoffMs} millis`),\n }),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n console.error(\n `Failed to publish batch to ${partition}: ${error}`\n );\n // Move to dead letter\n return {\n acknowledged: 0,\n messageIds: [],\n };\n })\n )\n );\n\n const newState = new Map(state.batches);\n newState.delete(partition);\n\n return {\n ...state,\n batches: newState,\n totalPublished: state.totalPublished + result.acknowledged,\n dead: result.acknowledged === 0 ? [...state.dead, ...newBatch] : state.dead,\n };\n } else {\n const newState = new Map(state.batches);\n newState.set(partition, newBatch);\n return { ...state, batches: newState };\n }\n }),\n (state) =>\n Effect.gen(function* () {\n let finalCount = state.totalPublished;\n\n for (const [partition, batch] of state.batches) {\n if (batch.length > 0) {\n const result = yield* publisher.publish(topic, partition, batch);\n finalCount += result.acknowledged;\n }\n }\n\n if (state.dead.length > 0) {\n console.error(\n `${state.dead.length} messages failed to publish and moved to dead letter`\n );\n }\n\n return finalCount;\n })\n );\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use message queues when:**\n\n- Events need to reach multiple downstream systems\n- Publishing shouldn't block (asynchronous)\n- Need ordering guarantees per partition\n- Subscribers may process at different speeds\n- Building event-driven architectures\n- Need reliable, durable message delivery\n\n⚠️ **Trade-offs:**\n\n- Added complexity (queue setup and management)\n- Latency for individual messages (buffered before publish)\n- Distributed systems challenges (exactly-once semantics)\n- Storage and infrastructure costs\n\n---\n\n## See Also\n\n- [Sink Pattern 1: Batch Insert](./sink-pattern-batch-insert-stream-records-into-database.mdx) - Bulk database persistence\n- [Sink Pattern 3: Write Lines to File](./sink-pattern-write-stream-lines-to-file.mdx) - File-based persistence\n- [Decouple Fibers with Queue/PubSub](./decouple-fibers-with-queue-pubsub.mdx) - Internal Effect queues\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals" - }, - { - "id": "sink-pattern-fall-back-to-alternative-sink-on-failure", - "title": "Sink Pattern 5: Fall Back to Alternative Sink on Failure", - "description": "Implement fallback sinks to handle failures gracefully and ensure data is persisted even when the primary destination is unavailable.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates a system that tries to write order records to a fast in-memory cache first, falls back to database if cache fails, and falls back to a dead letter file if database fails.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, Either, Data } from \"effect\";\n\ninterface Order {\n readonly orderId: string;\n readonly customerId: string;\n readonly total: number;\n readonly timestamp: number;\n}\n\nclass CacheSinkError extends Data.TaggedError(\"CacheSinkError\")<{\n readonly reason: string;\n}> {}\n\nclass DatabaseSinkError extends Data.TaggedError(\"DatabaseSinkError\")<{\n readonly reason: string;\n}> {}\n\n// Mock in-memory cache sink (fast but limited)\nconst createCacheSink = (): Sink.Sink => {\n const cache: Order[] = [];\n const MAX_CACHE_SIZE = 1000;\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n if (cache.length >= MAX_CACHE_SIZE) {\n yield* Effect.fail(\n new CacheSinkError({\n reason: `Cache full (${cache.length}/${MAX_CACHE_SIZE})`,\n })\n );\n }\n\n cache.push(order);\n console.log(`[CACHE] Cached order ${order.orderId}`);\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(`[CACHE] Final: ${count} orders in cache`);\n return count;\n })\n );\n};\n\n// Mock database sink (slower but reliable)\nconst createDatabaseSink = (): Sink.Sink => {\n const orders: Order[] = [];\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n // Simulate occasional database failures\n if (Math.random() < 0.1) {\n yield* Effect.fail(\n new DatabaseSinkError({\n reason: \"Connection timeout\",\n })\n );\n }\n\n orders.push(order);\n console.log(`[DATABASE] Persisted order ${order.orderId}`);\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(`[DATABASE] Final: ${count} orders in database`);\n return count;\n })\n );\n};\n\n// Mock file sink (always works but slow)\nconst createDeadLetterSink = (): Sink.Sink => {\n const deadLetters: Order[] = [];\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n deadLetters.push(order);\n console.log(\n `[DEAD-LETTER] Wrote order ${order.orderId} to dead letter file`\n );\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(\n `[DEAD-LETTER] Final: ${count} orders in dead letter file`\n );\n return count;\n })\n );\n};\n\n// Create a fallback sink that tries cache -> database -> file\nconst createFallbackSink = (): Sink.Sink<\n { readonly cached: number; readonly persisted: number; readonly deadLetters: number },\n never,\n Order\n> =>\n Sink.fold(\n { cached: 0, persisted: 0, deadLetters: 0 },\n (state, order) =>\n Effect.gen(function* () {\n // Try cache first\n const cacheResult = yield* createCacheSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(cacheResult)) {\n return {\n ...state,\n cached: state.cached + cacheResult.right,\n };\n }\n\n console.log(\n `[FALLBACK] Cache failed (${cacheResult.left.reason}), trying database`\n );\n\n // Cache failed, try database\n const dbResult = yield* createDatabaseSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(dbResult)) {\n return {\n ...state,\n persisted: state.persisted + dbResult.right,\n };\n }\n\n console.log(\n `[FALLBACK] Database failed (${dbResult.left.reason}), falling back to dead letter`\n );\n\n // Database failed, use dead letter\n const dlResult = yield* createDeadLetterSink()\n .pipe(Sink.feed(Chunk.of(order)));\n\n return {\n ...state,\n deadLetters: state.deadLetters + dlResult,\n };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Cached: ${state.cached}`);\n console.log(` Persisted: ${state.persisted}`);\n console.log(` Dead Letter: ${state.deadLetters}`);\n return state;\n })\n );\n\n// Simulate a stream of orders\nconst orderStream: Stream.Stream = Stream.fromIterable([\n {\n orderId: \"order-1\",\n customerId: \"cust-1\",\n total: 99.99,\n timestamp: Date.now(),\n },\n {\n orderId: \"order-2\",\n customerId: \"cust-2\",\n total: 149.99,\n timestamp: Date.now() + 100,\n },\n {\n orderId: \"order-3\",\n customerId: \"cust-1\",\n total: 49.99,\n timestamp: Date.now() + 200,\n },\n {\n orderId: \"order-4\",\n customerId: \"cust-3\",\n total: 199.99,\n timestamp: Date.now() + 300,\n },\n {\n orderId: \"order-5\",\n customerId: \"cust-2\",\n total: 89.99,\n timestamp: Date.now() + 400,\n },\n]);\n\n// Run the stream with fallback sink\nconst program = Effect.gen(function* () {\n const result = yield* orderStream.pipe(Stream.run(createFallbackSink()));\n console.log(`\\nTotal orders processed: ${result.cached + result.persisted + result.deadLetters}`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Tries cache first** (fast, limited capacity)\n2. **Falls back to database** if cache is full\n3. **Falls back to dead letter** if database fails\n4. **Tracks which sink** was used for each record\n5. **Reports summary** of where data went\n\n---", - "antiPattern": "", - "explanation": "Production systems need resilience:\n\n- **Primary failures**: Database down, network timeout, quota exceeded\n- **Progressive degradation**: Keep the system running, even at reduced capacity\n- **No data loss**: Fallback ensures data is persisted somewhere\n- **Operational flexibility**: Choose fallback based on failure type\n- **Monitoring**: Track when fallbacks are used to alert operators\n\nWithout fallback patterns:\n\n- System fails when primary destination fails\n- Data is lost if primary is unavailable\n- No clear signal that degradation occurred\n\nWith fallback sinks:\n\n- Stream continues even when primary fails\n- Data is safely persisted to alternative\n- Clear audit trail of which sink was used\n\n---", - "content": "## Guideline\n\nWhen consuming a stream to a primary destination that might fail, wrap it in a fallback pattern. If the primary sink fails, automatically redirect the stream to an alternative sink. This enables progressive degradation where the system degrades gracefully rather than failing completely.\n\n---\n\n## Rationale\n\nProduction systems need resilience:\n\n- **Primary failures**: Database down, network timeout, quota exceeded\n- **Progressive degradation**: Keep the system running, even at reduced capacity\n- **No data loss**: Fallback ensures data is persisted somewhere\n- **Operational flexibility**: Choose fallback based on failure type\n- **Monitoring**: Track when fallbacks are used to alert operators\n\nWithout fallback patterns:\n\n- System fails when primary destination fails\n- Data is lost if primary is unavailable\n- No clear signal that degradation occurred\n\nWith fallback sinks:\n\n- Stream continues even when primary fails\n- Data is safely persisted to alternative\n- Clear audit trail of which sink was used\n\n---\n\n## Good Example\n\nThis example demonstrates a system that tries to write order records to a fast in-memory cache first, falls back to database if cache fails, and falls back to a dead letter file if database fails.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, Either, Data } from \"effect\";\n\ninterface Order {\n readonly orderId: string;\n readonly customerId: string;\n readonly total: number;\n readonly timestamp: number;\n}\n\nclass CacheSinkError extends Data.TaggedError(\"CacheSinkError\")<{\n readonly reason: string;\n}> {}\n\nclass DatabaseSinkError extends Data.TaggedError(\"DatabaseSinkError\")<{\n readonly reason: string;\n}> {}\n\n// Mock in-memory cache sink (fast but limited)\nconst createCacheSink = (): Sink.Sink => {\n const cache: Order[] = [];\n const MAX_CACHE_SIZE = 1000;\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n if (cache.length >= MAX_CACHE_SIZE) {\n yield* Effect.fail(\n new CacheSinkError({\n reason: `Cache full (${cache.length}/${MAX_CACHE_SIZE})`,\n })\n );\n }\n\n cache.push(order);\n console.log(`[CACHE] Cached order ${order.orderId}`);\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(`[CACHE] Final: ${count} orders in cache`);\n return count;\n })\n );\n};\n\n// Mock database sink (slower but reliable)\nconst createDatabaseSink = (): Sink.Sink => {\n const orders: Order[] = [];\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n // Simulate occasional database failures\n if (Math.random() < 0.1) {\n yield* Effect.fail(\n new DatabaseSinkError({\n reason: \"Connection timeout\",\n })\n );\n }\n\n orders.push(order);\n console.log(`[DATABASE] Persisted order ${order.orderId}`);\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(`[DATABASE] Final: ${count} orders in database`);\n return count;\n })\n );\n};\n\n// Mock file sink (always works but slow)\nconst createDeadLetterSink = (): Sink.Sink => {\n const deadLetters: Order[] = [];\n\n return Sink.fold(\n 0,\n (count, order) =>\n Effect.gen(function* () {\n deadLetters.push(order);\n console.log(\n `[DEAD-LETTER] Wrote order ${order.orderId} to dead letter file`\n );\n return count + 1;\n }),\n (count) =>\n Effect.gen(function* () {\n console.log(\n `[DEAD-LETTER] Final: ${count} orders in dead letter file`\n );\n return count;\n })\n );\n};\n\n// Create a fallback sink that tries cache -> database -> file\nconst createFallbackSink = (): Sink.Sink<\n { readonly cached: number; readonly persisted: number; readonly deadLetters: number },\n never,\n Order\n> =>\n Sink.fold(\n { cached: 0, persisted: 0, deadLetters: 0 },\n (state, order) =>\n Effect.gen(function* () {\n // Try cache first\n const cacheResult = yield* createCacheSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(cacheResult)) {\n return {\n ...state,\n cached: state.cached + cacheResult.right,\n };\n }\n\n console.log(\n `[FALLBACK] Cache failed (${cacheResult.left.reason}), trying database`\n );\n\n // Cache failed, try database\n const dbResult = yield* createDatabaseSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(dbResult)) {\n return {\n ...state,\n persisted: state.persisted + dbResult.right,\n };\n }\n\n console.log(\n `[FALLBACK] Database failed (${dbResult.left.reason}), falling back to dead letter`\n );\n\n // Database failed, use dead letter\n const dlResult = yield* createDeadLetterSink()\n .pipe(Sink.feed(Chunk.of(order)));\n\n return {\n ...state,\n deadLetters: state.deadLetters + dlResult,\n };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Cached: ${state.cached}`);\n console.log(` Persisted: ${state.persisted}`);\n console.log(` Dead Letter: ${state.deadLetters}`);\n return state;\n })\n );\n\n// Simulate a stream of orders\nconst orderStream: Stream.Stream = Stream.fromIterable([\n {\n orderId: \"order-1\",\n customerId: \"cust-1\",\n total: 99.99,\n timestamp: Date.now(),\n },\n {\n orderId: \"order-2\",\n customerId: \"cust-2\",\n total: 149.99,\n timestamp: Date.now() + 100,\n },\n {\n orderId: \"order-3\",\n customerId: \"cust-1\",\n total: 49.99,\n timestamp: Date.now() + 200,\n },\n {\n orderId: \"order-4\",\n customerId: \"cust-3\",\n total: 199.99,\n timestamp: Date.now() + 300,\n },\n {\n orderId: \"order-5\",\n customerId: \"cust-2\",\n total: 89.99,\n timestamp: Date.now() + 400,\n },\n]);\n\n// Run the stream with fallback sink\nconst program = Effect.gen(function* () {\n const result = yield* orderStream.pipe(Stream.run(createFallbackSink()));\n console.log(`\\nTotal orders processed: ${result.cached + result.persisted + result.deadLetters}`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Tries cache first** (fast, limited capacity)\n2. **Falls back to database** if cache is full\n3. **Falls back to dead letter** if database fails\n4. **Tracks which sink** was used for each record\n5. **Reports summary** of where data went\n\n---\n\n## Advanced: Error-Type Based Routing\n\nRoute to different fallbacks based on the error type:\n\n```typescript\ntype FallbackStrategy = \"retry\" | \"cache\" | \"file\" | \"drop\";\n\nconst determineFallback = (error: CacheSinkError | DatabaseSinkError): FallbackStrategy => {\n if (error._tag === \"CacheSinkError\") {\n // Cache is full, try database\n return \"retry\";\n }\n if (error._tag === \"DatabaseSinkError\") {\n if (error.reason.includes(\"timeout\")) {\n // Network issue, retry\n return \"retry\";\n } else if (error.reason.includes(\"quota\")) {\n // Quota exceeded, use file\n return \"file\";\n }\n }\n // Unknown error, drop\n return \"drop\";\n};\n\nconst createIntelligentFallbackSink = (): Sink.Sink<\n { readonly cached: number; readonly persisted: number; readonly deadLetters: number; readonly dropped: number },\n never,\n Order\n> =>\n Sink.fold(\n { cached: 0, persisted: 0, deadLetters: 0, dropped: 0 },\n (state, order) =>\n Effect.gen(function* () {\n // Try cache\n const cacheAttempt = yield* createCacheSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(cacheAttempt)) {\n return { ...state, cached: state.cached + 1 };\n }\n\n const strategy = determineFallback(cacheAttempt.left);\n\n if (strategy === \"retry\") {\n // Try database\n const dbAttempt = yield* createDatabaseSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(Effect.either);\n\n if (Either.isRight(dbAttempt)) {\n return { ...state, persisted: state.persisted + 1 };\n }\n }\n\n if (strategy === \"file\" || strategy === \"retry\") {\n // Try dead letter\n const dlResult = yield* createDeadLetterSink()\n .pipe(Sink.feed(Chunk.of(order)));\n return { ...state, deadLetters: state.deadLetters + 1 };\n }\n\n // Drop\n console.log(`[DROPPED] Order ${order.orderId}`);\n return { ...state, dropped: state.dropped + 1 };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Cached: ${state.cached}`);\n console.log(` Persisted: ${state.persisted}`);\n console.log(` Dead Letter: ${state.deadLetters}`);\n console.log(` Dropped: ${state.dropped}`);\n return state;\n })\n );\n```\n\n---\n\n## Advanced: Fallback Chain with Retries\n\nRetry primary before falling back:\n\n```typescript\ninterface FallbackConfig {\n readonly maxRetries: number;\n readonly retryDelayMs: number;\n readonly backoffFactor: number;\n}\n\nconst createResilientFallbackSink = (\n config: FallbackConfig\n): Sink.Sink<{ readonly primary: number; readonly fallback: number }, never, Order> =>\n Sink.fold(\n { primary: 0, fallback: 0 },\n (state, order) =>\n Effect.gen(function* () {\n // Try primary with retries\n const primaryAttempt = yield* createCacheSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(\n Effect.retry({\n times: config.maxRetries,\n delay: () =>\n Effect.sleep(`${config.retryDelayMs} millis`),\n onFailure: (error, attempt) =>\n Effect.log(\n `Primary sink retry ${attempt}/${config.maxRetries}: ${error}`\n ),\n })\n )\n .pipe(Effect.either);\n\n if (Either.isRight(primaryAttempt)) {\n return { ...state, primary: state.primary + 1 };\n }\n\n // Primary exhausted retries, use fallback\n console.log(`[FALLBACK] Primary sink exhausted retries, using fallback`);\n\n const fallbackResult = yield* createDatabaseSink()\n .pipe(Sink.feed(Chunk.of(order)))\n .pipe(\n Effect.catchAll(() =>\n createDeadLetterSink().pipe(Sink.feed(Chunk.of(order)))\n )\n );\n\n return {\n ...state,\n fallback: state.fallback + 1,\n };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Primary: ${state.primary}`);\n console.log(` Fallback: ${state.fallback}`);\n return state;\n })\n );\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use fallback sinks when:**\n\n- Primary destination might fail (database, cache, service)\n- Need high availability with degradation\n- Different sinks have different characteristics (speed vs. reliability)\n- Want to avoid data loss when primary is unavailable\n- Building fault-tolerant systems\n\n⚠️ **Trade-offs:**\n\n- Increased complexity (managing multiple sinks)\n- Potential inconsistency (data in multiple places)\n- Monitoring complexity (need to track which sink was used)\n- Fallback may be slower/more expensive\n\n---\n\n## See Also\n\n- [Handle Errors with catchTag](./handle-errors-with-catch.mdx) - Error recovery basics\n- [Sink Pattern 1: Batch Insert](./sink-pattern-batch-insert-stream-records-into-database.mdx) - Primary sink pattern\n- [Sink Pattern 3: Write Lines to File](./sink-pattern-write-stream-lines-to-file.mdx) - File-based fallback\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals" - }, - { - "id": "sink-pattern-retry-failed-stream-operations", - "title": "Sink Pattern 6: Retry Failed Stream Operations", - "description": "Implement retry strategies in sinks to handle transient failures and improve resilience without manual intervention.", - "skillLevel": "intermediate", - "useCases": [ - "streams-sinks" - ], - "example": "This example demonstrates retrying database writes with exponential backoff, tracking attempts, and falling back on permanent failures.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, Duration, Schedule } from \"effect\";\n\ninterface UserRecord {\n readonly userId: string;\n readonly name: string;\n readonly email: string;\n}\n\nclass WriteError extends Error {\n readonly isTransient: boolean;\n\n constructor(message: string, isTransient: boolean = true) {\n super(message);\n this.name = \"WriteError\";\n this.isTransient = isTransient;\n }\n}\n\n// Mock database that occasionally fails\nconst database = {\n failureRate: 0.3, // 30% transient failure rate\n permanentFailureRate: 0.05, // 5% permanent failure rate\n\n insertUser: (user: UserRecord): Effect.Effect =>\n Effect.gen(function* () {\n const rand = Math.random();\n\n // Permanent failure (e.g., constraint violation)\n if (rand < database.permanentFailureRate) {\n throw new WriteError(\n `Permanent: User ${user.userId} already exists`,\n false\n );\n }\n\n // Transient failure (e.g., connection timeout)\n if (rand < database.permanentFailureRate + database.failureRate) {\n throw new WriteError(\n `Transient: Connection timeout writing ${user.userId}`,\n true\n );\n }\n\n // Success\n console.log(`✓ Wrote user ${user.userId}`);\n }),\n};\n\n// Retry configuration\ninterface RetryConfig {\n readonly maxAttempts: number;\n readonly initialDelayMs: number;\n readonly maxDelayMs: number;\n readonly backoffFactor: number;\n}\n\nconst defaultRetryConfig: RetryConfig = {\n maxAttempts: 5,\n initialDelayMs: 100, // Start with 100ms\n maxDelayMs: 5000, // Cap at 5 seconds\n backoffFactor: 2, // Double each time\n};\n\n// Result tracking\ninterface OperationResult {\n readonly succeeded: number;\n readonly transientFailures: number;\n readonly permanentFailures: number;\n readonly detailedStats: Array<{\n readonly userId: string;\n readonly attempts: number;\n readonly status: \"success\" | \"transient-failed\" | \"permanent-failed\";\n }>;\n}\n\n// Create a sink with retry logic\nconst createRetrySink = (config: RetryConfig): Sink.Sink =>\n Sink.fold(\n {\n succeeded: 0,\n transientFailures: 0,\n permanentFailures: 0,\n detailedStats: [],\n },\n (state, user) =>\n Effect.gen(function* () {\n let lastError: WriteError | null = null;\n let attempts = 0;\n\n // Retry loop\n for (attempts = 1; attempts <= config.maxAttempts; attempts++) {\n try {\n yield* database.insertUser(user);\n\n // Success!\n console.log(\n `[${user.userId}] Success on attempt ${attempts}/${config.maxAttempts}`\n );\n\n return {\n ...state,\n succeeded: state.succeeded + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts,\n status: \"success\",\n },\n ],\n };\n } catch (error) {\n lastError = error as WriteError;\n\n if (!lastError.isTransient) {\n // Permanent failure, don't retry\n console.log(\n `[${user.userId}] Permanent failure: ${lastError.message}`\n );\n\n return {\n ...state,\n permanentFailures: state.permanentFailures + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts,\n status: \"permanent-failed\",\n },\n ],\n };\n }\n\n // Transient failure, retry if attempts remain\n if (attempts < config.maxAttempts) {\n // Calculate delay with exponential backoff\n let delayMs = config.initialDelayMs * Math.pow(config.backoffFactor, attempts - 1);\n delayMs = Math.min(delayMs, config.maxDelayMs);\n\n // Add jitter (±10%)\n const jitter = delayMs * 0.1;\n delayMs = delayMs + (Math.random() - 0.5) * 2 * jitter;\n\n console.log(\n `[${user.userId}] Transient failure (attempt ${attempts}/${config.maxAttempts}): ${lastError.message}`\n );\n console.log(` Retrying in ${Math.round(delayMs)}ms...`);\n\n yield* Effect.sleep(Duration.millis(Math.round(delayMs)));\n }\n }\n }\n\n // All retries exhausted\n console.log(\n `[${user.userId}] Failed after ${config.maxAttempts} attempts`\n );\n\n return {\n ...state,\n transientFailures: state.transientFailures + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts: config.maxAttempts,\n status: \"transient-failed\",\n },\n ],\n };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Succeeded: ${state.succeeded}`);\n console.log(` Transient Failures: ${state.transientFailures}`);\n console.log(` Permanent Failures: ${state.permanentFailures}`);\n console.log(` Total: ${state.detailedStats.length}`);\n\n // Show detailed stats\n const failed = state.detailedStats.filter((s) => s.status !== \"success\");\n if (failed.length > 0) {\n console.log(`\\n[FAILURES]`);\n failed.forEach((stat) => {\n console.log(\n ` ${stat.userId}: ${stat.attempts} attempts (${stat.status})`\n );\n });\n }\n\n return state;\n })\n );\n\n// Simulate a stream of users to insert\nconst userStream: Stream.Stream = Stream.fromIterable([\n { userId: \"user-1\", name: \"Alice\", email: \"alice@example.com\" },\n { userId: \"user-2\", name: \"Bob\", email: \"bob@example.com\" },\n { userId: \"user-3\", name: \"Charlie\", email: \"charlie@example.com\" },\n { userId: \"user-4\", name: \"Diana\", email: \"diana@example.com\" },\n { userId: \"user-5\", name: \"Eve\", email: \"eve@example.com\" },\n]);\n\n// Run the stream with retry sink\nconst program = Effect.gen(function* () {\n const result = yield* userStream.pipe(Stream.run(createRetrySink(defaultRetryConfig)));\n console.log(`\\nProcessing complete.`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Attempts operation** up to max retries\n2. **Distinguishes transient vs. permanent failures**\n3. **Uses exponential backoff** to space retries\n4. **Adds jitter** to prevent thundering herd\n5. **Tracks detailed stats** for monitoring\n6. **Reports summary** of outcomes\n\n---", - "antiPattern": "", - "explanation": "Transient failures are common in distributed systems:\n\n- **Network timeouts**: Temporary connectivity issues resolve themselves\n- **Rate limiting**: Service recovers once rate limit window resets\n- **Temporary unavailability**: Services restart or scale up\n- **Circuit breaker trips**: Service recovers after backoff period\n\nWithout retry logic:\n\n- Every transient failure causes data loss or stream interruption\n- Manual intervention required to restart\n- System appears less reliable than it actually is\n\nWith intelligent retry logic:\n\n- Automatic recovery from transient failures\n- Exponential backoff prevents thundering herd\n- Clear visibility into which operations failed permanently\n- Data flows continuously despite temporary issues\n\n---", - "content": "## Guideline\n\nWhen consuming a stream to a destination that may experience transient failures (network timeouts, rate limiting, temporary unavailability), wrap the sink operation with a retry policy. Use exponential backoff to avoid overwhelming a recovering system while still recovering quickly.\n\n---\n\n## Rationale\n\nTransient failures are common in distributed systems:\n\n- **Network timeouts**: Temporary connectivity issues resolve themselves\n- **Rate limiting**: Service recovers once rate limit window resets\n- **Temporary unavailability**: Services restart or scale up\n- **Circuit breaker trips**: Service recovers after backoff period\n\nWithout retry logic:\n\n- Every transient failure causes data loss or stream interruption\n- Manual intervention required to restart\n- System appears less reliable than it actually is\n\nWith intelligent retry logic:\n\n- Automatic recovery from transient failures\n- Exponential backoff prevents thundering herd\n- Clear visibility into which operations failed permanently\n- Data flows continuously despite temporary issues\n\n---\n\n## Good Example\n\nThis example demonstrates retrying database writes with exponential backoff, tracking attempts, and falling back on permanent failures.\n\n```typescript\nimport { Effect, Stream, Sink, Chunk, Duration, Schedule } from \"effect\";\n\ninterface UserRecord {\n readonly userId: string;\n readonly name: string;\n readonly email: string;\n}\n\nclass WriteError extends Error {\n readonly isTransient: boolean;\n\n constructor(message: string, isTransient: boolean = true) {\n super(message);\n this.name = \"WriteError\";\n this.isTransient = isTransient;\n }\n}\n\n// Mock database that occasionally fails\nconst database = {\n failureRate: 0.3, // 30% transient failure rate\n permanentFailureRate: 0.05, // 5% permanent failure rate\n\n insertUser: (user: UserRecord): Effect.Effect =>\n Effect.gen(function* () {\n const rand = Math.random();\n\n // Permanent failure (e.g., constraint violation)\n if (rand < database.permanentFailureRate) {\n throw new WriteError(\n `Permanent: User ${user.userId} already exists`,\n false\n );\n }\n\n // Transient failure (e.g., connection timeout)\n if (rand < database.permanentFailureRate + database.failureRate) {\n throw new WriteError(\n `Transient: Connection timeout writing ${user.userId}`,\n true\n );\n }\n\n // Success\n console.log(`✓ Wrote user ${user.userId}`);\n }),\n};\n\n// Retry configuration\ninterface RetryConfig {\n readonly maxAttempts: number;\n readonly initialDelayMs: number;\n readonly maxDelayMs: number;\n readonly backoffFactor: number;\n}\n\nconst defaultRetryConfig: RetryConfig = {\n maxAttempts: 5,\n initialDelayMs: 100, // Start with 100ms\n maxDelayMs: 5000, // Cap at 5 seconds\n backoffFactor: 2, // Double each time\n};\n\n// Result tracking\ninterface OperationResult {\n readonly succeeded: number;\n readonly transientFailures: number;\n readonly permanentFailures: number;\n readonly detailedStats: Array<{\n readonly userId: string;\n readonly attempts: number;\n readonly status: \"success\" | \"transient-failed\" | \"permanent-failed\";\n }>;\n}\n\n// Create a sink with retry logic\nconst createRetrySink = (config: RetryConfig): Sink.Sink =>\n Sink.fold(\n {\n succeeded: 0,\n transientFailures: 0,\n permanentFailures: 0,\n detailedStats: [],\n },\n (state, user) =>\n Effect.gen(function* () {\n let lastError: WriteError | null = null;\n let attempts = 0;\n\n // Retry loop\n for (attempts = 1; attempts <= config.maxAttempts; attempts++) {\n try {\n yield* database.insertUser(user);\n\n // Success!\n console.log(\n `[${user.userId}] Success on attempt ${attempts}/${config.maxAttempts}`\n );\n\n return {\n ...state,\n succeeded: state.succeeded + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts,\n status: \"success\",\n },\n ],\n };\n } catch (error) {\n lastError = error as WriteError;\n\n if (!lastError.isTransient) {\n // Permanent failure, don't retry\n console.log(\n `[${user.userId}] Permanent failure: ${lastError.message}`\n );\n\n return {\n ...state,\n permanentFailures: state.permanentFailures + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts,\n status: \"permanent-failed\",\n },\n ],\n };\n }\n\n // Transient failure, retry if attempts remain\n if (attempts < config.maxAttempts) {\n // Calculate delay with exponential backoff\n let delayMs = config.initialDelayMs * Math.pow(config.backoffFactor, attempts - 1);\n delayMs = Math.min(delayMs, config.maxDelayMs);\n\n // Add jitter (±10%)\n const jitter = delayMs * 0.1;\n delayMs = delayMs + (Math.random() - 0.5) * 2 * jitter;\n\n console.log(\n `[${user.userId}] Transient failure (attempt ${attempts}/${config.maxAttempts}): ${lastError.message}`\n );\n console.log(` Retrying in ${Math.round(delayMs)}ms...`);\n\n yield* Effect.sleep(Duration.millis(Math.round(delayMs)));\n }\n }\n }\n\n // All retries exhausted\n console.log(\n `[${user.userId}] Failed after ${config.maxAttempts} attempts`\n );\n\n return {\n ...state,\n transientFailures: state.transientFailures + 1,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts: config.maxAttempts,\n status: \"transient-failed\",\n },\n ],\n };\n }),\n (state) =>\n Effect.gen(function* () {\n console.log(`\\n[SUMMARY]`);\n console.log(` Succeeded: ${state.succeeded}`);\n console.log(` Transient Failures: ${state.transientFailures}`);\n console.log(` Permanent Failures: ${state.permanentFailures}`);\n console.log(` Total: ${state.detailedStats.length}`);\n\n // Show detailed stats\n const failed = state.detailedStats.filter((s) => s.status !== \"success\");\n if (failed.length > 0) {\n console.log(`\\n[FAILURES]`);\n failed.forEach((stat) => {\n console.log(\n ` ${stat.userId}: ${stat.attempts} attempts (${stat.status})`\n );\n });\n }\n\n return state;\n })\n );\n\n// Simulate a stream of users to insert\nconst userStream: Stream.Stream = Stream.fromIterable([\n { userId: \"user-1\", name: \"Alice\", email: \"alice@example.com\" },\n { userId: \"user-2\", name: \"Bob\", email: \"bob@example.com\" },\n { userId: \"user-3\", name: \"Charlie\", email: \"charlie@example.com\" },\n { userId: \"user-4\", name: \"Diana\", email: \"diana@example.com\" },\n { userId: \"user-5\", name: \"Eve\", email: \"eve@example.com\" },\n]);\n\n// Run the stream with retry sink\nconst program = Effect.gen(function* () {\n const result = yield* userStream.pipe(Stream.run(createRetrySink(defaultRetryConfig)));\n console.log(`\\nProcessing complete.`);\n});\n\nEffect.runPromise(program);\n```\n\nThis pattern:\n\n1. **Attempts operation** up to max retries\n2. **Distinguishes transient vs. permanent failures**\n3. **Uses exponential backoff** to space retries\n4. **Adds jitter** to prevent thundering herd\n5. **Tracks detailed stats** for monitoring\n6. **Reports summary** of outcomes\n\n---\n\n## Advanced: Effect-Based Retry Schedules\n\nUse Effect's built-in retry and schedule combinators:\n\n```typescript\nconst createEffectRetrySink = (\n maxRetries: number = 5\n): Sink.Sink =>\n Sink.fold(\n {\n succeeded: 0,\n transientFailures: 0,\n permanentFailures: 0,\n detailedStats: [],\n },\n (state, user) =>\n Effect.gen(function* () {\n const result = yield* database\n .insertUser(user)\n .pipe(\n // Retry with exponential backoff\n Effect.retry(\n Schedule.exponential(\"100 millis\").pipe(\n Schedule.addDelay((attempt) =>\n Duration.millis(Math.random() * 10) // Add jitter\n ),\n Schedule.compose(\n Schedule.recurs(maxRetries),\n Schedule.resetAfter(Duration.seconds(30)) // Reset backoff after 30s\n )\n )\n ),\n // Handle permanent failures\n Effect.catchAll((error: WriteError) =>\n Effect.gen(function* () {\n if (!error.isTransient) {\n return \"permanent-failed\" as const;\n }\n return \"transient-failed\" as const;\n })\n )\n );\n\n if (result === \"success\") {\n return {\n ...state,\n succeeded: state.succeeded + 1,\n };\n } else if (result === \"permanent-failed\") {\n return {\n ...state,\n permanentFailures: state.permanentFailures + 1,\n };\n } else {\n return {\n ...state,\n transientFailures: state.transientFailures + 1,\n };\n }\n }),\n (state) => Effect.succeed(state)\n );\n```\n\n---\n\n## Advanced: Adaptive Retry with Metrics\n\nAdjust retry strategy based on success rate:\n\n```typescript\ninterface AdaptiveRetryConfig {\n readonly initialMaxRetries: number;\n readonly minMaxRetries: number;\n readonly maxMaxRetries: number;\n readonly successThreshold: number; // Increase retries if success rate below this\n}\n\nconst createAdaptiveRetrySink = (\n initialConfig: AdaptiveRetryConfig\n): Sink.Sink =>\n Sink.fold(\n {\n succeeded: 0,\n transientFailures: 0,\n permanentFailures: 0,\n detailedStats: [],\n currentMaxRetries: initialConfig.initialMaxRetries,\n recentSuccessRate: 1.0,\n },\n (state, user) =>\n Effect.gen(function* () {\n // Adjust max retries based on recent success rate\n let maxRetries = state.currentMaxRetries;\n\n if (state.recentSuccessRate < initialConfig.successThreshold) {\n // Low success rate, increase retries\n maxRetries = Math.min(\n maxRetries + 1,\n initialConfig.maxMaxRetries\n );\n console.log(\n `[ADAPTIVE] Success rate ${(state.recentSuccessRate * 100).toFixed(1)}% < ${(initialConfig.successThreshold * 100).toFixed(1)}%, increasing retries to ${maxRetries}`\n );\n } else if (state.recentSuccessRate > initialConfig.successThreshold * 1.2) {\n // High success rate, decrease retries\n maxRetries = Math.max(\n maxRetries - 1,\n initialConfig.minMaxRetries\n );\n console.log(\n `[ADAPTIVE] Success rate ${(state.recentSuccessRate * 100).toFixed(1)}%, decreasing retries to ${maxRetries}`\n );\n }\n\n // Attempt write with current retry config\n let attempts = 0;\n let succeeded = false;\n\n for (attempts = 1; attempts <= maxRetries; attempts++) {\n try {\n yield* database.insertUser(user);\n succeeded = true;\n break;\n } catch (error) {\n const writeError = error as WriteError;\n if (!writeError.isTransient) break;\n if (attempts < maxRetries) {\n yield* Effect.sleep(\n Duration.millis(100 * Math.pow(2, attempts - 1))\n );\n }\n }\n }\n\n // Update success rate (exponential moving average)\n const newSuccessRate =\n state.recentSuccessRate * 0.7 + (succeeded ? 1 : 0) * 0.3;\n\n return {\n succeeded: state.succeeded + (succeeded ? 1 : 0),\n transientFailures:\n state.transientFailures + (succeeded ? 0 : 1),\n permanentFailures: state.permanentFailures,\n detailedStats: [\n ...state.detailedStats,\n {\n userId: user.userId,\n attempts,\n status: succeeded ? \"success\" : \"transient-failed\",\n },\n ],\n currentMaxRetries: maxRetries,\n recentSuccessRate: newSuccessRate,\n };\n }),\n (state) => Effect.succeed(state)\n );\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use retry logic when:**\n\n- Failures are often transient (network, temporary unavailability)\n- Can distinguish between transient and permanent failures\n- Want automatic recovery without manual intervention\n- Need to handle rate limiting gracefully\n- Building fault-tolerant systems\n\n⚠️ **Trade-offs:**\n\n- Retries increase latency (each failed attempt adds delay)\n- May exacerbate problems if overused (retry storm)\n- Requires careful tuning of backoff parameters\n- Some failures can't be recovered by retry\n\n---\n\n## See Also\n\n- [Handle Flaky Operations with Retry & Timeout](./handle-flaky-operations-with-retry-timeout.mdx) - Retry fundamentals\n- [Sink Pattern 5: Fall Back](./sink-pattern-fall-back-to-alternative-sink-on-failure.mdx) - Fallback patterns\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream fundamentals\n- [Control Repetition with Schedule](./control-repetition-with-schedule.mdx) - Schedule patterns" - }, - { - "id": "solve-promise-problems-with-effect", - "title": "Solve Promise Problems with Effect", - "description": "Recognize that Effect solves the core limitations of Promises: untyped errors, no dependency injection, and no cancellation.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "This code is type-safe, testable, and cancellable. The signature `Effect.Effect` tells us everything we need to know.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\ninterface DbErrorType {\n readonly _tag: \"DbError\";\n readonly message: string;\n}\n\nconst DbError = Data.tagged(\"DbError\");\n\ninterface User {\n name: string;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n findById: (id: number): Effect.Effect =>\n Effect.try({\n try: () => ({ name: `User ${id}` }),\n catch: () => DbError({ message: \"Failed to find user\" }),\n }),\n }),\n}) {}\n\nconst findUser = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient;\n return yield* client.findById(id);\n });\n\n// Demonstrate how Effect solves promise problems\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Solving Promise Problems with Effect ===\");\n\n // Problem 1: Proper error handling (no more try/catch hell)\n yield* Effect.logInfo(\"1. Demonstrating type-safe error handling:\");\n\n const result1 = yield* findUser(123).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Handled error: ${error.message}`);\n return { name: \"Default User\" };\n })\n )\n );\n yield* Effect.logInfo(`Found user: ${result1.name}`);\n\n // Problem 2: Easy composition and chaining\n yield* Effect.logInfo(\"\\n2. Demonstrating easy composition:\");\n\n const composedOperation = Effect.gen(function* () {\n const user1 = yield* findUser(1);\n const user2 = yield* findUser(2);\n yield* Effect.logInfo(`Composed result: ${user1.name} and ${user2.name}`);\n return [user1, user2];\n });\n\n yield* composedOperation;\n\n // Problem 3: Resource management and cleanup\n yield* Effect.logInfo(\"\\n3. Demonstrating resource management:\");\n\n const resourceOperation = Effect.gen(function* () {\n yield* Effect.logInfo(\"Acquiring resource...\");\n const resource = \"database-connection\";\n\n yield* Effect.addFinalizer(() => Effect.logInfo(\"Cleaning up resource...\"));\n\n const user = yield* findUser(456);\n yield* Effect.logInfo(`Used resource to get: ${user.name}`);\n\n return user;\n }).pipe(Effect.scoped);\n\n yield* resourceOperation;\n\n yield* Effect.logInfo(\"\\n✅ All operations completed successfully!\");\n});\n\nEffect.runPromise(Effect.provide(program, HttpClient.Default));\n```\n\n---", - "antiPattern": "This `Promise`-based function has several hidden problems that Effect solves:\n\n- What happens if `db.findUser` rejects? The error is untyped (`any`).\n- Where does `db` come from? It's a hidden dependency, making this function hard to test.\n- If the operation is slow, how do we cancel it? We can't.\n\n```typescript\n// ❌ This function has hidden dependencies and untyped errors.\nasync function findUserUnsafely(id: number): Promise {\n try {\n const user = await db.findUser(id); // `db` is a hidden global or import\n return user;\n } catch (error) {\n // `error` is of type `any`. We don't know what it is.\n // We might log it and re-throw, but we can't handle it safely.\n throw error;\n }\n}\n```", - "explanation": "While `async/await` is great for simple cases, building large, robust applications with `Promise`s reveals these critical gaps. Effect addresses each one directly:\n\n- **Typed Errors:** The `E` channel in `Effect` forces you to handle specific, known error types, eliminating an entire class of runtime bugs.\n- **Dependency Injection:** The `R` channel provides a powerful, built-in system for declaring and providing dependencies (`Layer`s), making your code modular and testable.\n- **Cancellation (Interruption):** Effect's structured concurrency and `Fiber` model provide robust, built-in cancellation. When an effect is interrupted, Effect guarantees that its cleanup logic (finalizers) will be run.\n\nUnderstanding that Effect was built specifically to solve these problems is key to appreciating its design and power.\n\n---", - "content": "## Guideline\n\nRecognize that `Effect` is not just a \"better Promise,\" but a fundamentally different construct designed to solve the core limitations of native `Promise`s in TypeScript:\n\n1. **Untyped Errors:** Promises can reject with `any` value, forcing `try/catch` blocks and unsafe type checks.\n2. **No Dependency Injection:** Promises have no built-in way to declare or manage dependencies, leading to tightly coupled code.\n3. **No Cancellation:** Once a `Promise` starts, it cannot be cancelled from the outside.\n\n---\n\n## Rationale\n\nWhile `async/await` is great for simple cases, building large, robust applications with `Promise`s reveals these critical gaps. Effect addresses each one directly:\n\n- **Typed Errors:** The `E` channel in `Effect` forces you to handle specific, known error types, eliminating an entire class of runtime bugs.\n- **Dependency Injection:** The `R` channel provides a powerful, built-in system for declaring and providing dependencies (`Layer`s), making your code modular and testable.\n- **Cancellation (Interruption):** Effect's structured concurrency and `Fiber` model provide robust, built-in cancellation. When an effect is interrupted, Effect guarantees that its cleanup logic (finalizers) will be run.\n\nUnderstanding that Effect was built specifically to solve these problems is key to appreciating its design and power.\n\n---\n\n## Good Example (The Effect Way)\n\nThis code is type-safe, testable, and cancellable. The signature `Effect.Effect` tells us everything we need to know.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\ninterface DbErrorType {\n readonly _tag: \"DbError\";\n readonly message: string;\n}\n\nconst DbError = Data.tagged(\"DbError\");\n\ninterface User {\n name: string;\n}\n\nclass HttpClient extends Effect.Service()(\"HttpClient\", {\n sync: () => ({\n findById: (id: number): Effect.Effect =>\n Effect.try({\n try: () => ({ name: `User ${id}` }),\n catch: () => DbError({ message: \"Failed to find user\" }),\n }),\n }),\n}) {}\n\nconst findUser = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient;\n return yield* client.findById(id);\n });\n\n// Demonstrate how Effect solves promise problems\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Solving Promise Problems with Effect ===\");\n\n // Problem 1: Proper error handling (no more try/catch hell)\n yield* Effect.logInfo(\"1. Demonstrating type-safe error handling:\");\n\n const result1 = yield* findUser(123).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Handled error: ${error.message}`);\n return { name: \"Default User\" };\n })\n )\n );\n yield* Effect.logInfo(`Found user: ${result1.name}`);\n\n // Problem 2: Easy composition and chaining\n yield* Effect.logInfo(\"\\n2. Demonstrating easy composition:\");\n\n const composedOperation = Effect.gen(function* () {\n const user1 = yield* findUser(1);\n const user2 = yield* findUser(2);\n yield* Effect.logInfo(`Composed result: ${user1.name} and ${user2.name}`);\n return [user1, user2];\n });\n\n yield* composedOperation;\n\n // Problem 3: Resource management and cleanup\n yield* Effect.logInfo(\"\\n3. Demonstrating resource management:\");\n\n const resourceOperation = Effect.gen(function* () {\n yield* Effect.logInfo(\"Acquiring resource...\");\n const resource = \"database-connection\";\n\n yield* Effect.addFinalizer(() => Effect.logInfo(\"Cleaning up resource...\"));\n\n const user = yield* findUser(456);\n yield* Effect.logInfo(`Used resource to get: ${user.name}`);\n\n return user;\n }).pipe(Effect.scoped);\n\n yield* resourceOperation;\n\n yield* Effect.logInfo(\"\\n✅ All operations completed successfully!\");\n});\n\nEffect.runPromise(Effect.provide(program, HttpClient.Default));\n```\n\n---\n\n## Anti-Pattern (The Promise Way)\n\nThis `Promise`-based function has several hidden problems that Effect solves:\n\n- What happens if `db.findUser` rejects? The error is untyped (`any`).\n- Where does `db` come from? It's a hidden dependency, making this function hard to test.\n- If the operation is slow, how do we cancel it? We can't.\n\n```typescript\n// ❌ This function has hidden dependencies and untyped errors.\nasync function findUserUnsafely(id: number): Promise {\n try {\n const user = await db.findUser(id); // `db` is a hidden global or import\n return user;\n } catch (error) {\n // `error` is of type `any`. We don't know what it is.\n // We might log it and re-throw, but we can't handle it safely.\n throw error;\n }\n}\n```" - }, - { - "id": "state-management-pattern-synchronized-ref", - "title": "State Management Pattern 1: Synchronized Reference with SynchronizedRef", - "description": "Use SynchronizedRef for thread-safe mutable state that must be updated consistently across concurrent operations, with atomic modifications.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates synchronized reference patterns.\n\n```typescript\nimport { Effect, Ref, Fiber, Deferred } from \"effect\";\n\ninterface Counter {\n readonly value: number;\n readonly updates: number;\n}\n\ninterface Account {\n readonly balance: number;\n readonly transactions: string[];\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[SYNCHRONIZED REFERENCES] Concurrent state management\\n`\n );\n\n // Example 1: Basic counter with atomic updates\n console.log(`[1] Atomic counter increments:\\n`);\n\n const counter = yield* Ref.make({\n value: 0,\n updates: 0,\n });\n\n // Simulate 5 concurrent increments\n const incrementTasks = Array.from({ length: 5 }, (_, i) =>\n Effect.gen(function* () {\n for (let j = 0; j < 20; j++) {\n yield* Ref.modify(counter, (current) => [\n undefined,\n {\n value: current.value + 1,\n updates: current.updates + 1,\n },\n ]);\n\n if (j === 0 || j === 19) {\n yield* Effect.log(\n `[FIBER ${i}] Increment ${j === 0 ? \"start\" : \"end\"}`\n );\n }\n }\n })\n );\n\n // Run concurrently\n yield* Effect.all(incrementTasks, { concurrency: \"unbounded\" });\n\n const finalCounter = yield* Ref.get(counter);\n\n yield* Effect.log(\n `[RESULT] Counter: ${finalCounter.value} (expected 100)`\n );\n yield* Effect.log(\n `[RESULT] Updates: ${finalCounter.updates} (expected 100)\\n`\n );\n\n // Example 2: Bank account with transaction isolation\n console.log(`[2] Account with atomic transfers:\\n`);\n\n const account = yield* Ref.make({\n balance: 1000,\n transactions: [],\n });\n\n const transfer = (amount: number, description: string) =>\n Ref.modify(account, (current) => {\n if (current.balance < amount) {\n // Insufficient funds, don't modify\n return [\n { success: false, reason: \"insufficient-funds\" },\n current, // Unchanged\n ];\n }\n\n // Atomic: deduct + record transaction\n return [\n { success: true, reason: \"transferred\" },\n {\n balance: current.balance - amount,\n transactions: [\n ...current.transactions,\n `${description}: -$${amount}`,\n ],\n },\n ];\n });\n\n // Test transfer\n const t1 = yield* transfer(100, \"Coffee\");\n\n yield* Effect.log(`[TRANSFER 1] ${t1.success ? \"✓\" : \"✗\"} ${t1.reason}`);\n\n const t2 = yield* transfer(2000, \"Electronics\");\n\n yield* Effect.log(`[TRANSFER 2] ${t2.success ? \"✓\" : \"✗\"} ${t2.reason}`);\n\n const t3 = yield* transfer(200, \"Groceries\");\n\n yield* Effect.log(`[TRANSFER 3] ${t3.success ? \"✓\" : \"✗\"} ${t3.reason}\\n`);\n\n // Example 3: Concurrent reads don't block writes\n console.log(`[3] Concurrent reads and writes:\\n`);\n\n const state = yield* Ref.make({ value: 0, readers: 0 });\n\n const read = Effect.gen(function* () {\n const snapshot = yield* Ref.get(state);\n\n yield* Effect.log(\n `[READ] Got value: ${snapshot.value}`\n );\n\n return snapshot.value;\n });\n\n const write = (newValue: number) =>\n Ref.set(state, { value: newValue, readers: 0 });\n\n // Concurrent operations\n const mixed = Effect.all(\n [\n read,\n write(10),\n read,\n write(20),\n read,\n ],\n { concurrency: \"unbounded\" }\n );\n\n yield* mixed;\n\n // Example 4: Compare-and-set pattern (retry on failure)\n console.log(`\\n[4] Compare-and-set (optimistic updates):\\n`);\n\n const versionedState = yield* Ref.make({ version: 0, data: \"initial\" });\n\n const updateWithVersion = (newData: string) =>\n Effect.gen(function* () {\n let retries = 0;\n\n while (retries < 3) {\n const current = yield* Ref.get(versionedState);\n\n // Try to update (check-and-set)\n const result = yield* Ref.modify(versionedState, (s) => {\n if (s.version === current.version) {\n // No concurrent update, proceed\n return [\n { success: true },\n {\n version: s.version + 1,\n data: newData,\n },\n ];\n }\n\n // Version changed, conflict\n return [{ success: false }, s];\n });\n\n if (result.success) {\n yield* Effect.log(\n `[CAS] Updated on attempt ${retries + 1}`\n );\n\n return true;\n }\n\n retries++;\n\n yield* Effect.log(\n `[CAS] Conflict detected, retrying (attempt ${retries + 1})`\n );\n }\n\n return false;\n });\n\n const casResult = yield* updateWithVersion(\"updated-data\");\n\n yield* Effect.log(`[CAS] Success: ${casResult}\\n`);\n\n // Example 5: State with subscriptions (notify on change)\n console.log(`[5] State changes with notification:\\n`);\n\n interface Notification {\n oldValue: unknown;\n newValue: unknown;\n timestamp: Date;\n }\n\n const observedState = yield* Ref.make<{ value: number; lastChange: Date }>({\n value: 0,\n lastChange: new Date(),\n });\n\n const updateAndNotify = (newValue: number) =>\n Ref.modify(observedState, (current) => {\n const notification: Notification = {\n oldValue: current.value,\n newValue,\n timestamp: new Date(),\n };\n\n yield* Effect.log(\n `[NOTIFY] ${current.value} → ${newValue} at ${notification.timestamp.toISOString()}`\n );\n\n return [\n notification,\n {\n value: newValue,\n lastChange: notification.timestamp,\n },\n ];\n });\n\n // Trigger changes\n for (const val of [5, 10, 15]) {\n yield* updateAndNotify(val);\n }\n\n // Example 6: Atomic batch updates\n console.log(`\\n[6] Batch atomic updates:\\n`);\n\n interface BatchState {\n items: string[];\n locked: boolean;\n version: number;\n }\n\n const batchState = yield* Ref.make({\n items: [],\n locked: false,\n version: 0,\n });\n\n const addItems = (newItems: string[]) =>\n Ref.modify(batchState, (current) => {\n // All items added atomically\n return [\n { added: newItems.length },\n {\n items: [...current.items, ...newItems],\n locked: false,\n version: current.version + 1,\n },\n ];\n });\n\n const batch1 = yield* addItems([\"item1\", \"item2\", \"item3\"]);\n\n yield* Effect.log(\n `[BATCH 1] Added ${batch1.added} items`\n );\n\n const batch2 = yield* addItems([\"item4\", \"item5\"]);\n\n yield* Effect.log(\n `[BATCH 2] Added ${batch2.added} items`\n );\n\n const finalBatch = yield* Ref.get(batchState);\n\n yield* Effect.log(\n `[RESULT] Total items: ${finalBatch.items.length}, Version: ${finalBatch.version}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Shared mutable state without synchronization causes problems:\n\n**Problem 1: Data races**\n- Fiber A reads counter (value: 5)\n- Fiber B reads counter (value: 5)\n- Fiber A writes counter + 1 (value: 6)\n- Fiber B writes counter + 1 (value: 6)\n- Expected: 7, Got: 6 (lost update)\n\n**Problem 2: Inconsistent snapshots**\n- Transaction reads user.balance (100)\n- User spent money elsewhere\n- Transaction reads user.balance again (90)\n- Now inconsistent within same transaction\n\n**Problem 3: Race conditions**\n- Check inventory (10 items)\n- Check passes\n- Before purchase, inventory goes to 0 (race)\n- Purchase fails, user frustrated\n\n**Problem 4: Deadlocks**\n- Fiber A locks state, tries to acquire another\n- Fiber B holds that state, tries to acquire first\n- Both stuck forever\n\nSolutions:\n\n**Atomic operations**:\n- Read and modify as single operation\n- No intermediate states visible\n- No race window\n- Guaranteed consistency\n\n**Compare-and-set**:\n- \"If value is X, change to Y\" (atomic)\n- Fails if another fiber changed it\n- Retry automatically\n- No locks needed\n\n**Snapshot isolation**:\n- Read complete snapshot\n- All operations see consistent view\n- Modifications build on snapshot\n- Merge changes safely\n\n---", - "content": "## Guideline\n\nSynchronized references manage shared state safely:\n\n- **Atomic updates**: All-or-nothing modifications\n- **Consistent reads**: Snapshot consistency\n- **Lock-free optimism**: Try updates, retry on failure\n- **Compare-and-set**: Atomic check-and-update\n- **Transaction safety**: Multiple operations as one\n\nPattern: `Ref.make()`, `Ref.modify()`, `Ref.set()`, `Ref.get()`\n\n---\n\n## Rationale\n\nShared mutable state without synchronization causes problems:\n\n**Problem 1: Data races**\n- Fiber A reads counter (value: 5)\n- Fiber B reads counter (value: 5)\n- Fiber A writes counter + 1 (value: 6)\n- Fiber B writes counter + 1 (value: 6)\n- Expected: 7, Got: 6 (lost update)\n\n**Problem 2: Inconsistent snapshots**\n- Transaction reads user.balance (100)\n- User spent money elsewhere\n- Transaction reads user.balance again (90)\n- Now inconsistent within same transaction\n\n**Problem 3: Race conditions**\n- Check inventory (10 items)\n- Check passes\n- Before purchase, inventory goes to 0 (race)\n- Purchase fails, user frustrated\n\n**Problem 4: Deadlocks**\n- Fiber A locks state, tries to acquire another\n- Fiber B holds that state, tries to acquire first\n- Both stuck forever\n\nSolutions:\n\n**Atomic operations**:\n- Read and modify as single operation\n- No intermediate states visible\n- No race window\n- Guaranteed consistency\n\n**Compare-and-set**:\n- \"If value is X, change to Y\" (atomic)\n- Fails if another fiber changed it\n- Retry automatically\n- No locks needed\n\n**Snapshot isolation**:\n- Read complete snapshot\n- All operations see consistent view\n- Modifications build on snapshot\n- Merge changes safely\n\n---\n\n## Good Example\n\nThis example demonstrates synchronized reference patterns.\n\n```typescript\nimport { Effect, Ref, Fiber, Deferred } from \"effect\";\n\ninterface Counter {\n readonly value: number;\n readonly updates: number;\n}\n\ninterface Account {\n readonly balance: number;\n readonly transactions: string[];\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[SYNCHRONIZED REFERENCES] Concurrent state management\\n`\n );\n\n // Example 1: Basic counter with atomic updates\n console.log(`[1] Atomic counter increments:\\n`);\n\n const counter = yield* Ref.make({\n value: 0,\n updates: 0,\n });\n\n // Simulate 5 concurrent increments\n const incrementTasks = Array.from({ length: 5 }, (_, i) =>\n Effect.gen(function* () {\n for (let j = 0; j < 20; j++) {\n yield* Ref.modify(counter, (current) => [\n undefined,\n {\n value: current.value + 1,\n updates: current.updates + 1,\n },\n ]);\n\n if (j === 0 || j === 19) {\n yield* Effect.log(\n `[FIBER ${i}] Increment ${j === 0 ? \"start\" : \"end\"}`\n );\n }\n }\n })\n );\n\n // Run concurrently\n yield* Effect.all(incrementTasks, { concurrency: \"unbounded\" });\n\n const finalCounter = yield* Ref.get(counter);\n\n yield* Effect.log(\n `[RESULT] Counter: ${finalCounter.value} (expected 100)`\n );\n yield* Effect.log(\n `[RESULT] Updates: ${finalCounter.updates} (expected 100)\\n`\n );\n\n // Example 2: Bank account with transaction isolation\n console.log(`[2] Account with atomic transfers:\\n`);\n\n const account = yield* Ref.make({\n balance: 1000,\n transactions: [],\n });\n\n const transfer = (amount: number, description: string) =>\n Ref.modify(account, (current) => {\n if (current.balance < amount) {\n // Insufficient funds, don't modify\n return [\n { success: false, reason: \"insufficient-funds\" },\n current, // Unchanged\n ];\n }\n\n // Atomic: deduct + record transaction\n return [\n { success: true, reason: \"transferred\" },\n {\n balance: current.balance - amount,\n transactions: [\n ...current.transactions,\n `${description}: -$${amount}`,\n ],\n },\n ];\n });\n\n // Test transfer\n const t1 = yield* transfer(100, \"Coffee\");\n\n yield* Effect.log(`[TRANSFER 1] ${t1.success ? \"✓\" : \"✗\"} ${t1.reason}`);\n\n const t2 = yield* transfer(2000, \"Electronics\");\n\n yield* Effect.log(`[TRANSFER 2] ${t2.success ? \"✓\" : \"✗\"} ${t2.reason}`);\n\n const t3 = yield* transfer(200, \"Groceries\");\n\n yield* Effect.log(`[TRANSFER 3] ${t3.success ? \"✓\" : \"✗\"} ${t3.reason}\\n`);\n\n // Example 3: Concurrent reads don't block writes\n console.log(`[3] Concurrent reads and writes:\\n`);\n\n const state = yield* Ref.make({ value: 0, readers: 0 });\n\n const read = Effect.gen(function* () {\n const snapshot = yield* Ref.get(state);\n\n yield* Effect.log(\n `[READ] Got value: ${snapshot.value}`\n );\n\n return snapshot.value;\n });\n\n const write = (newValue: number) =>\n Ref.set(state, { value: newValue, readers: 0 });\n\n // Concurrent operations\n const mixed = Effect.all(\n [\n read,\n write(10),\n read,\n write(20),\n read,\n ],\n { concurrency: \"unbounded\" }\n );\n\n yield* mixed;\n\n // Example 4: Compare-and-set pattern (retry on failure)\n console.log(`\\n[4] Compare-and-set (optimistic updates):\\n`);\n\n const versionedState = yield* Ref.make({ version: 0, data: \"initial\" });\n\n const updateWithVersion = (newData: string) =>\n Effect.gen(function* () {\n let retries = 0;\n\n while (retries < 3) {\n const current = yield* Ref.get(versionedState);\n\n // Try to update (check-and-set)\n const result = yield* Ref.modify(versionedState, (s) => {\n if (s.version === current.version) {\n // No concurrent update, proceed\n return [\n { success: true },\n {\n version: s.version + 1,\n data: newData,\n },\n ];\n }\n\n // Version changed, conflict\n return [{ success: false }, s];\n });\n\n if (result.success) {\n yield* Effect.log(\n `[CAS] Updated on attempt ${retries + 1}`\n );\n\n return true;\n }\n\n retries++;\n\n yield* Effect.log(\n `[CAS] Conflict detected, retrying (attempt ${retries + 1})`\n );\n }\n\n return false;\n });\n\n const casResult = yield* updateWithVersion(\"updated-data\");\n\n yield* Effect.log(`[CAS] Success: ${casResult}\\n`);\n\n // Example 5: State with subscriptions (notify on change)\n console.log(`[5] State changes with notification:\\n`);\n\n interface Notification {\n oldValue: unknown;\n newValue: unknown;\n timestamp: Date;\n }\n\n const observedState = yield* Ref.make<{ value: number; lastChange: Date }>({\n value: 0,\n lastChange: new Date(),\n });\n\n const updateAndNotify = (newValue: number) =>\n Ref.modify(observedState, (current) => {\n const notification: Notification = {\n oldValue: current.value,\n newValue,\n timestamp: new Date(),\n };\n\n yield* Effect.log(\n `[NOTIFY] ${current.value} → ${newValue} at ${notification.timestamp.toISOString()}`\n );\n\n return [\n notification,\n {\n value: newValue,\n lastChange: notification.timestamp,\n },\n ];\n });\n\n // Trigger changes\n for (const val of [5, 10, 15]) {\n yield* updateAndNotify(val);\n }\n\n // Example 6: Atomic batch updates\n console.log(`\\n[6] Batch atomic updates:\\n`);\n\n interface BatchState {\n items: string[];\n locked: boolean;\n version: number;\n }\n\n const batchState = yield* Ref.make({\n items: [],\n locked: false,\n version: 0,\n });\n\n const addItems = (newItems: string[]) =>\n Ref.modify(batchState, (current) => {\n // All items added atomically\n return [\n { added: newItems.length },\n {\n items: [...current.items, ...newItems],\n locked: false,\n version: current.version + 1,\n },\n ];\n });\n\n const batch1 = yield* addItems([\"item1\", \"item2\", \"item3\"]);\n\n yield* Effect.log(\n `[BATCH 1] Added ${batch1.added} items`\n );\n\n const batch2 = yield* addItems([\"item4\", \"item5\"]);\n\n yield* Effect.log(\n `[BATCH 2] Added ${batch2.added} items`\n );\n\n const finalBatch = yield* Ref.get(batchState);\n\n yield* Effect.log(\n `[RESULT] Total items: ${finalBatch.items.length}, Version: ${finalBatch.version}`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Lock-Free Queue with SynchronizedRef\n\nBuild a queue without explicit locks:\n\n```typescript\ninterface Queue {\n enqueue: (value: T) => Effect.Effect;\n dequeue: () => Effect.Effect;\n size: () => Effect.Effect;\n}\n\nconst createLockFreeQueue = (): Effect.Effect> =>\n Effect.gen(function* () {\n const state = yield* Ref.make([]);\n\n return {\n enqueue: (value: T) =>\n Ref.modify(state, (items) => [undefined, [...items, value]]),\n\n dequeue: () =>\n Ref.modify(state, (items) => {\n if (items.length === 0) {\n return [undefined, items];\n }\n\n const [first, ...rest] = items;\n\n return [first, rest];\n }),\n\n size: () => Ref.get(state).pipe(\n Effect.map((items) => items.length)\n ),\n };\n });\n```\n\n---\n\n## Advanced: Retry on Conflict\n\nAutomatic retry for failed compare-and-set:\n\n```typescript\nconst updateWithRetry = (\n ref: Ref.Ref,\n f: (current: T) => T\n): Effect.Effect =>\n Effect.gen(function* () {\n const maxRetries = 10;\n\n for (let i = 0; i < maxRetries; i++) {\n const current = yield* Ref.get(ref);\n const updated = f(current);\n\n const result = yield* Ref.modify(ref, (s) => {\n if (s === current) {\n return [{ success: true }, updated];\n }\n\n return [{ success: false }, s];\n });\n\n if (result.success) {\n return updated;\n }\n\n // Exponential backoff on conflict\n yield* Effect.sleep(`${Math.pow(2, i)} millis`);\n }\n\n yield* Effect.fail(new Error(\"Update failed after retries\"));\n });\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use SynchronizedRef when:**\n- Shared state across fibers\n- Atomic operations needed\n- Consistency matters\n- High concurrency\n- No external locks available\n\n✅ **Use Ref.modify() when:**\n- Read + write must be atomic\n- Conditional updates\n- Conflict detection needed\n\n⚠️ **Trade-offs:**\n- Retry overhead on conflicts\n- Memory allocation in modify\n- Harder to reason about\n- Performance impact with high contention\n\n---\n\n## Performance Tuning\n\n| Scenario | Strategy | Benefit |\n| --- | --- | --- |\n| **Low contention** | Direct Ref | Minimal overhead |\n| **High contention** | Exponential backoff | Reduce retry storms |\n| **Batch updates** | Single modify | Atomic batch |\n| **Reader-heavy** | Get-only | No modification cost |\n\n---\n\n## See Also\n\n- [Concurrency Pattern 3: Latch Coordination](./concurrency-pattern-coordinate-with-latch.mdx) - Multi-fiber sync\n- [Concurrency Pattern 4: Queue Distribution](./concurrency-pattern-queue-work-distribution.mdx) - Work queues\n- [Stream Pattern 4: Stateful Operations](./stream-pattern-stateful-operations.mdx) - Stream state\n- [State Management Pattern 2: SubscriptionRef](./state-management-pattern-subscription-ref.mdx) - Observable state" - }, - { - "id": "state-management-pattern-subscription-ref", - "title": "State Management Pattern 2: Observable State with SubscriptionRef", - "description": "Combine Ref with PubSub to create observable state where changes trigger notifications, enabling reactive state management.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This example demonstrates observable state patterns.\n\n```typescript\nimport { Effect, Ref, PubSub, Stream } from \"effect\";\n\ninterface StateChange {\n readonly previous: T;\n readonly current: T;\n readonly timestamp: Date;\n readonly reason: string;\n}\n\ninterface Observable {\n readonly get: () => Effect.Effect;\n readonly set: (value: T, reason: string) => Effect.Effect;\n readonly subscribe: () => Stream.Stream>;\n readonly modify: (f: (current: T) => T, reason: string) => Effect.Effect;\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[OBSERVABLE STATE] Reactive state management\\n`\n );\n\n // Create observable\n const createObservable = (initialValue: T): Effect.Effect> =>\n Effect.gen(function* () {\n const state = yield* Ref.make(initialValue);\n const changeStream = yield* PubSub.unbounded>();\n\n return {\n get: () => Ref.get(state),\n\n set: (value: T, reason: string) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(state);\n\n if (previous === value) {\n return; // No change\n }\n\n yield* Ref.set(state, value);\n\n const change: StateChange = {\n previous,\n current: value,\n timestamp: new Date(),\n reason,\n };\n\n yield* PubSub.publish(changeStream, change);\n }),\n\n subscribe: () =>\n PubSub.subscribe(changeStream),\n\n modify: (f: (current: T) => T, reason: string) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(state);\n const updated = f(previous);\n\n if (previous === updated) {\n return; // No change\n }\n\n yield* Ref.set(state, updated);\n\n const change: StateChange = {\n previous,\n current: updated,\n timestamp: new Date(),\n reason,\n };\n\n yield* PubSub.publish(changeStream, change);\n }),\n };\n });\n\n // Example 1: Basic observable counter\n console.log(`[1] Observable counter:\\n`);\n\n const counter = yield* createObservable(0);\n\n // Subscribe to changes\n const printChanges = counter.subscribe().pipe(\n Stream.tap((change) =>\n Effect.log(\n `[CHANGE] ${change.previous} → ${change.current} (${change.reason})`\n )\n ),\n Stream.take(5), // Limit to 5 changes for demo\n Stream.runDrain\n );\n\n // Make changes\n yield* counter.set(1, \"increment\");\n yield* counter.set(2, \"increment\");\n yield* counter.set(5, \"reset\");\n\n // Wait for changes to be processed\n yield* Effect.sleep(\"100 millis\");\n\n // Example 2: Derived state (computed values)\n console.log(`\\n[2] Derived state (total from items):\\n`);\n\n interface ShoppingCart {\n readonly items: Array<{ id: string; price: number }>;\n readonly discount: number;\n }\n\n const cart = yield* createObservable({\n items: [],\n discount: 0,\n });\n\n const computeTotal = (state: ShoppingCart): number => {\n const subtotal = state.items.reduce((sum, item) => sum + item.price, 0);\n return subtotal * (1 - state.discount);\n };\n\n // Create derived observable\n const total = yield* createObservable(computeTotal(yield* cart.get()));\n\n // Subscribe to cart changes, update total\n const updateTotalOnCartChange = cart.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n const newTotal = computeTotal(change.current);\n\n yield* total.set(newTotal, \"recalculated-from-cart\");\n\n yield* Effect.log(\n `[TOTAL] Recalculated: $${newTotal.toFixed(2)}`\n );\n })\n ),\n Stream.take(10),\n Stream.runDrain\n );\n\n // Make cart changes\n yield* cart.modify(\n (state) => ({\n ...state,\n items: [\n ...state.items,\n { id: \"item1\", price: 19.99 },\n ],\n }),\n \"add-item\"\n );\n\n yield* cart.modify(\n (state) => ({\n ...state,\n items: [\n ...state.items,\n { id: \"item2\", price: 29.99 },\n ],\n }),\n \"add-item\"\n );\n\n yield* cart.modify(\n (state) => ({\n ...state,\n discount: 0.1,\n }),\n \"apply-discount\"\n );\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 3: Effect triggering on state change\n console.log(`\\n[3] Effects triggered by state changes:\\n`);\n\n type AppStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n const appStatus = yield* createObservable(\"idle\");\n\n // Define effects for each status\n const handleStatusChange = appStatus.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[STATUS] ${change.previous} → ${change.current}`\n );\n\n switch (change.current) {\n case \"loading\":\n yield* Effect.log(`[EFFECT] Starting loading animation`);\n break;\n\n case \"ready\":\n yield* Effect.log(`[EFFECT] Hiding spinner, showing content`);\n break;\n\n case \"error\":\n yield* Effect.log(`[EFFECT] Showing error message`);\n yield* Effect.log(`[TELEMETRY] Logging error event`);\n break;\n\n default:\n yield* Effect.log(`[EFFECT] Resetting UI`);\n }\n })\n ),\n Stream.take(6),\n Stream.runDrain\n );\n\n // Trigger status changes\n yield* appStatus.set(\"loading\", \"user-clicked\");\n yield* appStatus.set(\"ready\", \"data-loaded\");\n yield* appStatus.set(\"loading\", \"user-refreshed\");\n yield* appStatus.set(\"error\", \"api-failed\");\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 4: Multi-level state aggregation\n console.log(`\\n[4] Aggregated state from multiple sources:\\n`);\n\n interface UserProfile {\n name: string;\n email: string;\n role: string;\n }\n\n interface AppState {\n user: UserProfile | null;\n notifications: number;\n theme: \"light\" | \"dark\";\n }\n\n const appState = yield* createObservable({\n user: null,\n notifications: 0,\n theme: \"light\",\n });\n\n // Subscribe to track changes\n const trackChanges = appState.subscribe().pipe(\n Stream.tap((change) => {\n if (change.current.user && !change.previous.user) {\n return Effect.log(`[EVENT] User logged in: ${change.current.user.name}`);\n }\n\n if (!change.current.user && change.previous.user) {\n return Effect.log(`[EVENT] User logged out`);\n }\n\n if (change.current.notifications !== change.previous.notifications) {\n return Effect.log(\n `[NOTIFY] ${change.current.notifications} notifications`\n );\n }\n\n if (change.current.theme !== change.previous.theme) {\n return Effect.log(`[THEME] Switched to ${change.current.theme}`);\n }\n\n return Effect.succeed(undefined);\n }),\n Stream.take(10),\n Stream.runDrain\n );\n\n // Make changes\n yield* appState.modify(\n (state) => ({\n ...state,\n user: { name: \"Alice\", email: \"alice@example.com\", role: \"admin\" },\n }),\n \"user-login\"\n );\n\n yield* appState.modify(\n (state) => ({\n ...state,\n notifications: 5,\n }),\n \"new-notifications\"\n );\n\n yield* appState.modify(\n (state) => ({\n ...state,\n theme: \"dark\",\n }),\n \"user-preference\"\n );\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 5: State snapshot and history\n console.log(`\\n[5] State history tracking:\\n`);\n\n interface HistoryEntry {\n value: T;\n timestamp: Date;\n reason: string;\n }\n\n const history = yield* Ref.make[]>([]);\n\n const trackedCounter = yield* createObservable(0);\n\n const trackHistory = trackedCounter.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n yield* Ref.modify(history, (h) => [\n undefined,\n [\n ...h,\n {\n value: change.current,\n timestamp: change.timestamp,\n reason: change.reason,\n },\n ],\n ]);\n\n yield* Effect.log(\n `[HISTORY] Recorded: ${change.current} (${change.reason})`\n );\n })\n ),\n Stream.take(5),\n Stream.runDrain\n );\n\n // Make changes\n for (let i = 1; i <= 4; i++) {\n yield* trackedCounter.set(i, `step-${i}`);\n }\n\n yield* Effect.sleep(\"200 millis\");\n\n // Print history\n const hist = yield* Ref.get(history);\n\n yield* Effect.log(`\\n[HISTORY] ${hist.length} entries:`);\n\n for (const entry of hist) {\n yield* Effect.log(\n ` - ${entry.value} (${entry.reason})`\n );\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Passive state causes problems:\n\n**Problem 1: Stale UI**\n- State changes in backend\n- UI doesn't know\n- User sees old data\n- Manual refresh required\n\n**Problem 2: Cascading updates**\n- User changes form field\n- Need to update 5 other fields\n- Manual imperative code\n- Fragile, easy to miss one\n\n**Problem 3: Derived state**\n- Total = sum of items\n- Manual update on each item change\n- Duplicate code everywhere\n- Bug: total not updated when items change\n\n**Problem 4: Side effects**\n- User enables feature\n- Multiple things must happen\n- Analytics, notifications, API calls\n- All imperative, hard to maintain\n\nSolutions:\n\n**Observable state**:\n- State change = event\n- Subscribers notified\n- UI binds directly\n- Auto-updates\n\n**Reactive flows**:\n- Define how state flows\n- `newTotal = items.sum()`\n- Automatic recalculation\n- No manual updates\n\n**Side effect chaining**:\n- When state changes to \"complete\"\n- Send notification\n- Log event\n- Trigger cleanup\n- All declaratively\n\n---", - "content": "## Guideline\n\nObservable state enables reactive patterns:\n\n- **State binding**: UI binds to state, auto-updates on change\n- **Subscribers**: Multiple handlers notified on change\n- **Event streams**: Changes become event streams\n- **Derived state**: Compute values from state changes\n- **Effect triggering**: Changes trigger side effects\n\nPattern: Combine `Ref` + `PubSub` or custom subscription system\n\n---\n\n## Rationale\n\nPassive state causes problems:\n\n**Problem 1: Stale UI**\n- State changes in backend\n- UI doesn't know\n- User sees old data\n- Manual refresh required\n\n**Problem 2: Cascading updates**\n- User changes form field\n- Need to update 5 other fields\n- Manual imperative code\n- Fragile, easy to miss one\n\n**Problem 3: Derived state**\n- Total = sum of items\n- Manual update on each item change\n- Duplicate code everywhere\n- Bug: total not updated when items change\n\n**Problem 4: Side effects**\n- User enables feature\n- Multiple things must happen\n- Analytics, notifications, API calls\n- All imperative, hard to maintain\n\nSolutions:\n\n**Observable state**:\n- State change = event\n- Subscribers notified\n- UI binds directly\n- Auto-updates\n\n**Reactive flows**:\n- Define how state flows\n- `newTotal = items.sum()`\n- Automatic recalculation\n- No manual updates\n\n**Side effect chaining**:\n- When state changes to \"complete\"\n- Send notification\n- Log event\n- Trigger cleanup\n- All declaratively\n\n---\n\n## Good Example\n\nThis example demonstrates observable state patterns.\n\n```typescript\nimport { Effect, Ref, PubSub, Stream } from \"effect\";\n\ninterface StateChange {\n readonly previous: T;\n readonly current: T;\n readonly timestamp: Date;\n readonly reason: string;\n}\n\ninterface Observable {\n readonly get: () => Effect.Effect;\n readonly set: (value: T, reason: string) => Effect.Effect;\n readonly subscribe: () => Stream.Stream>;\n readonly modify: (f: (current: T) => T, reason: string) => Effect.Effect;\n}\n\nconst program = Effect.gen(function* () {\n console.log(\n `\\n[OBSERVABLE STATE] Reactive state management\\n`\n );\n\n // Create observable\n const createObservable = (initialValue: T): Effect.Effect> =>\n Effect.gen(function* () {\n const state = yield* Ref.make(initialValue);\n const changeStream = yield* PubSub.unbounded>();\n\n return {\n get: () => Ref.get(state),\n\n set: (value: T, reason: string) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(state);\n\n if (previous === value) {\n return; // No change\n }\n\n yield* Ref.set(state, value);\n\n const change: StateChange = {\n previous,\n current: value,\n timestamp: new Date(),\n reason,\n };\n\n yield* PubSub.publish(changeStream, change);\n }),\n\n subscribe: () =>\n PubSub.subscribe(changeStream),\n\n modify: (f: (current: T) => T, reason: string) =>\n Effect.gen(function* () {\n const previous = yield* Ref.get(state);\n const updated = f(previous);\n\n if (previous === updated) {\n return; // No change\n }\n\n yield* Ref.set(state, updated);\n\n const change: StateChange = {\n previous,\n current: updated,\n timestamp: new Date(),\n reason,\n };\n\n yield* PubSub.publish(changeStream, change);\n }),\n };\n });\n\n // Example 1: Basic observable counter\n console.log(`[1] Observable counter:\\n`);\n\n const counter = yield* createObservable(0);\n\n // Subscribe to changes\n const printChanges = counter.subscribe().pipe(\n Stream.tap((change) =>\n Effect.log(\n `[CHANGE] ${change.previous} → ${change.current} (${change.reason})`\n )\n ),\n Stream.take(5), // Limit to 5 changes for demo\n Stream.runDrain\n );\n\n // Make changes\n yield* counter.set(1, \"increment\");\n yield* counter.set(2, \"increment\");\n yield* counter.set(5, \"reset\");\n\n // Wait for changes to be processed\n yield* Effect.sleep(\"100 millis\");\n\n // Example 2: Derived state (computed values)\n console.log(`\\n[2] Derived state (total from items):\\n`);\n\n interface ShoppingCart {\n readonly items: Array<{ id: string; price: number }>;\n readonly discount: number;\n }\n\n const cart = yield* createObservable({\n items: [],\n discount: 0,\n });\n\n const computeTotal = (state: ShoppingCart): number => {\n const subtotal = state.items.reduce((sum, item) => sum + item.price, 0);\n return subtotal * (1 - state.discount);\n };\n\n // Create derived observable\n const total = yield* createObservable(computeTotal(yield* cart.get()));\n\n // Subscribe to cart changes, update total\n const updateTotalOnCartChange = cart.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n const newTotal = computeTotal(change.current);\n\n yield* total.set(newTotal, \"recalculated-from-cart\");\n\n yield* Effect.log(\n `[TOTAL] Recalculated: $${newTotal.toFixed(2)}`\n );\n })\n ),\n Stream.take(10),\n Stream.runDrain\n );\n\n // Make cart changes\n yield* cart.modify(\n (state) => ({\n ...state,\n items: [\n ...state.items,\n { id: \"item1\", price: 19.99 },\n ],\n }),\n \"add-item\"\n );\n\n yield* cart.modify(\n (state) => ({\n ...state,\n items: [\n ...state.items,\n { id: \"item2\", price: 29.99 },\n ],\n }),\n \"add-item\"\n );\n\n yield* cart.modify(\n (state) => ({\n ...state,\n discount: 0.1,\n }),\n \"apply-discount\"\n );\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 3: Effect triggering on state change\n console.log(`\\n[3] Effects triggered by state changes:\\n`);\n\n type AppStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\n const appStatus = yield* createObservable(\"idle\");\n\n // Define effects for each status\n const handleStatusChange = appStatus.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[STATUS] ${change.previous} → ${change.current}`\n );\n\n switch (change.current) {\n case \"loading\":\n yield* Effect.log(`[EFFECT] Starting loading animation`);\n break;\n\n case \"ready\":\n yield* Effect.log(`[EFFECT] Hiding spinner, showing content`);\n break;\n\n case \"error\":\n yield* Effect.log(`[EFFECT] Showing error message`);\n yield* Effect.log(`[TELEMETRY] Logging error event`);\n break;\n\n default:\n yield* Effect.log(`[EFFECT] Resetting UI`);\n }\n })\n ),\n Stream.take(6),\n Stream.runDrain\n );\n\n // Trigger status changes\n yield* appStatus.set(\"loading\", \"user-clicked\");\n yield* appStatus.set(\"ready\", \"data-loaded\");\n yield* appStatus.set(\"loading\", \"user-refreshed\");\n yield* appStatus.set(\"error\", \"api-failed\");\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 4: Multi-level state aggregation\n console.log(`\\n[4] Aggregated state from multiple sources:\\n`);\n\n interface UserProfile {\n name: string;\n email: string;\n role: string;\n }\n\n interface AppState {\n user: UserProfile | null;\n notifications: number;\n theme: \"light\" | \"dark\";\n }\n\n const appState = yield* createObservable({\n user: null,\n notifications: 0,\n theme: \"light\",\n });\n\n // Subscribe to track changes\n const trackChanges = appState.subscribe().pipe(\n Stream.tap((change) => {\n if (change.current.user && !change.previous.user) {\n return Effect.log(`[EVENT] User logged in: ${change.current.user.name}`);\n }\n\n if (!change.current.user && change.previous.user) {\n return Effect.log(`[EVENT] User logged out`);\n }\n\n if (change.current.notifications !== change.previous.notifications) {\n return Effect.log(\n `[NOTIFY] ${change.current.notifications} notifications`\n );\n }\n\n if (change.current.theme !== change.previous.theme) {\n return Effect.log(`[THEME] Switched to ${change.current.theme}`);\n }\n\n return Effect.succeed(undefined);\n }),\n Stream.take(10),\n Stream.runDrain\n );\n\n // Make changes\n yield* appState.modify(\n (state) => ({\n ...state,\n user: { name: \"Alice\", email: \"alice@example.com\", role: \"admin\" },\n }),\n \"user-login\"\n );\n\n yield* appState.modify(\n (state) => ({\n ...state,\n notifications: 5,\n }),\n \"new-notifications\"\n );\n\n yield* appState.modify(\n (state) => ({\n ...state,\n theme: \"dark\",\n }),\n \"user-preference\"\n );\n\n yield* Effect.sleep(\"200 millis\");\n\n // Example 5: State snapshot and history\n console.log(`\\n[5] State history tracking:\\n`);\n\n interface HistoryEntry {\n value: T;\n timestamp: Date;\n reason: string;\n }\n\n const history = yield* Ref.make[]>([]);\n\n const trackedCounter = yield* createObservable(0);\n\n const trackHistory = trackedCounter.subscribe().pipe(\n Stream.tap((change) =>\n Effect.gen(function* () {\n yield* Ref.modify(history, (h) => [\n undefined,\n [\n ...h,\n {\n value: change.current,\n timestamp: change.timestamp,\n reason: change.reason,\n },\n ],\n ]);\n\n yield* Effect.log(\n `[HISTORY] Recorded: ${change.current} (${change.reason})`\n );\n })\n ),\n Stream.take(5),\n Stream.runDrain\n );\n\n // Make changes\n for (let i = 1; i <= 4; i++) {\n yield* trackedCounter.set(i, `step-${i}`);\n }\n\n yield* Effect.sleep(\"200 millis\");\n\n // Print history\n const hist = yield* Ref.get(history);\n\n yield* Effect.log(`\\n[HISTORY] ${hist.length} entries:`);\n\n for (const entry of hist) {\n yield* Effect.log(\n ` - ${entry.value} (${entry.reason})`\n );\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Computed Observable Properties\n\nDerive multiple values from state:\n\n```typescript\nconst createComputedObservable = (\n source: Observable,\n compute: (value: T) => U\n): Effect.Effect> =>\n Effect.gen(function* () {\n const computed = yield* createObservable(\n compute(yield* source.get())\n );\n\n source.subscribe().pipe(\n Stream.tap((change) =>\n computed.set(compute(change.current), `derived-from-${change.reason}`)\n ),\n Stream.runDrain\n );\n\n return computed;\n });\n\n// Usage: Compute full name from user\nconst createFullNameObservable = (user: Observable) =>\n createComputedObservable(\n user,\n (u) => `${u.firstName} ${u.lastName}`\n );\n```\n\n---\n\n## Advanced: Transactional Updates\n\nGroup related state changes:\n\n```typescript\nconst transactional = (\n observable: Observable,\n updates: Array<(current: T) => T>,\n reason: string\n): Effect.Effect =>\n Effect.gen(function* () {\n let current = yield* observable.get();\n\n // Apply all updates\n for (const update of updates) {\n current = update(current);\n }\n\n // Single notification\n yield* observable.set(current, reason);\n });\n\n// Usage: Update multiple fields as one transaction\nconst transaction = transactional(\n appState,\n [\n (s) => ({ ...s, notifications: 0 }),\n (s) => ({ ...s, lastSeen: new Date() }),\n ],\n \"mark-all-as-read\"\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use observable state when:**\n- UI binds to state\n- Multiple subscribers\n- Derived state needed\n- Effect triggering needed\n- State-driven architecture\n\n✅ **Use PubSub when:**\n- Fan-out notifications\n- Multiple subscribers\n- Stream processing\n- Decoupled components\n\n⚠️ **Trade-offs:**\n- Memory overhead (subscriptions)\n- Notification latency\n- Complexity of debugging\n- Potential for circular updates\n\n---\n\n## Patterns for Reactivity\n\n| Pattern | When | Trade-off |\n| --- | --- | --- |\n| **Polling** | Simple state | Inefficient |\n| **Observable** | Multiple subscribers | Overhead |\n| **Computed** | Derived values | Update cycles |\n| **Transactional** | Related changes | Complexity |\n\n---\n\n## See Also\n\n- [Concurrency Pattern 5: PubSub Broadcasting](./concurrency-pattern-pubsub-event-broadcast.mdx) - Event broadcasting\n- [State Management Pattern 1: SynchronizedRef](./state-management-pattern-synchronized-ref.mdx) - Thread-safe state\n- [Stream Pattern 4: Stateful Operations](./stream-pattern-stateful-operations.mdx) - Stream state\n- [Stream Pattern 1: Map & Filter](./stream-pattern-map-filter-transformations.mdx) - Stream transforms" - }, - { - "id": "stream-pattern-map-filter-transformations", - "title": "Stream Pattern 1: Transform Streams with Map and Filter", - "description": "Use map and filter combinators to transform stream elements declaratively, creating pipelines that reshape data without materializing intermediate results.", - "skillLevel": "beginner", - "useCases": [ - "streams" - ], - "example": "This example demonstrates transforming a stream of raw data through multiple stages.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface RawLogEntry {\n readonly timestamp: string;\n readonly level: string;\n readonly message: string;\n}\n\ninterface ProcessedLog {\n readonly date: Date;\n readonly severity: \"low\" | \"medium\" | \"high\";\n readonly normalizedMessage: string;\n}\n\n// Create a stream of raw log entries\nconst createLogStream = (): Stream.Stream =>\n Stream.fromIterable([\n { timestamp: \"2025-12-17T09:00:00Z\", level: \"DEBUG\", message: \"App starting\" },\n { timestamp: \"2025-12-17T09:01:00Z\", level: \"INFO\", message: \"Connected to DB\" },\n { timestamp: \"2025-12-17T09:02:00Z\", level: \"ERROR\", message: \"Query timeout\" },\n { timestamp: \"2025-12-17T09:03:00Z\", level: \"DEBUG\", message: \"Retry initiated\" },\n { timestamp: \"2025-12-17T09:04:00Z\", level: \"WARN\", message: \"Connection degraded\" },\n { timestamp: \"2025-12-17T09:05:00Z\", level: \"INFO\", message: \"Recovered\" },\n ]);\n\n// Transform: Parse timestamp\nconst parseTimestamp = (entry: RawLogEntry): RawLogEntry => ({\n ...entry,\n timestamp: entry.timestamp, // Already ISO, but could parse here\n});\n\n// Transform: Map log level to severity\nconst mapSeverity = (level: string): \"low\" | \"medium\" | \"high\" => {\n if (level === \"DEBUG\" || level === \"INFO\") return \"low\";\n if (level === \"WARN\") return \"medium\";\n return \"high\";\n};\n\n// Transform: Normalize message\nconst normalizeMessage = (message: string): string =>\n message.toLowerCase().trim();\n\n// Filter: Keep only important logs\nconst isImportant = (entry: RawLogEntry): boolean => {\n return entry.level !== \"DEBUG\";\n};\n\n// Main pipeline\nconst program = Effect.gen(function* () {\n console.log(`\\n[STREAM] Processing log stream with map/filter\\n`);\n\n // Create and transform stream\n const transformedStream = createLogStream().pipe(\n // Filter: Keep only non-debug logs\n Stream.filter((entry) => {\n const important = isImportant(entry);\n console.log(\n `[FILTER] ${entry.level} → ${important ? \"✓ kept\" : \"✗ filtered out\"}`\n );\n return important;\n }),\n\n // Map: Extract date\n Stream.map((entry) => {\n const date = new Date(entry.timestamp);\n console.log(`[MAP-1] Parsed date: ${date.toISOString()}`);\n return { ...entry, parsedDate: date };\n }),\n\n // Map: Normalize and map severity\n Stream.map((entry) => {\n const processed: ProcessedLog = {\n date: entry.parsedDate,\n severity: mapSeverity(entry.level),\n normalizedMessage: normalizeMessage(entry.message),\n };\n console.log(\n `[MAP-2] Transformed: ${entry.level} → ${processed.severity}`\n );\n return processed;\n })\n );\n\n // Collect all transformed logs\n const results = yield* transformedStream.pipe(\n Stream.runCollect\n );\n\n console.log(`\\n[RESULTS]`);\n console.log(` Total logs: ${results.length}`);\n\n Chunk.forEach(results, (log) => {\n console.log(\n ` - [${log.severity.toUpperCase()}] ${log.date.toISOString()}: ${log.normalizedMessage}`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\nOutput shows lazy evaluation and filtering:\n```\n[STREAM] Processing log stream with map/filter\n\n[FILTER] DEBUG → ✗ filtered out\n[FILTER] INFO → ✓ kept\n[MAP-1] Parsed date: 2025-12-17T09:01:00.000Z\n[MAP-2] Transformed: INFO → low\n[FILTER] ERROR → ✓ kept\n[MAP-1] Parsed date: 2025-12-17T09:02:00.000Z\n[MAP-2] Transformed: ERROR → high\n...\n\n[RESULTS]\n Total logs: 5\n - [LOW] 2025-12-17T09:01:00.000Z: connected to db\n - [HIGH] 2025-12-17T09:02:00.000Z: query timeout\n ...\n```\n\n---", - "antiPattern": "", - "explanation": "Streaming data transformations without map/filter create problems:\n\n- **Buffering**: Must collect all data before transforming\n- **Code verbosity**: Manual loops for each transformation\n- **Memory usage**: Large intermediate arrays\n- **Composability**: Hard to chain operations\n\nMap/filter enable:\n\n- **Lazy evaluation**: Transform on-demand\n- **Composable**: Chain operations naturally\n- **Memory efficient**: No intermediate collections\n- **Expressive**: Declare intent clearly\n\nReal-world example: Processing logs\n- **Without map/filter**: Collect logs, filter by level, map to objects, transform fields\n- **With map/filter**: `logStream.pipe(Stream.filter(...), Stream.map(...))`\n\n---", - "content": "## Guideline\n\nUse `Stream.map` and `Stream.filter` to transform streams:\n\n- **map**: Transform each element (1→1)\n- **filter**: Keep elements matching predicate (N→N, discards some)\n- **Chain**: Compose multiple operations\n- **Lazy**: Elements transformed on demand (no buffering)\n\nPattern: `stream.pipe(Stream.map(...), Stream.filter(...))`\n\n---\n\n## Rationale\n\nStreaming data transformations without map/filter create problems:\n\n- **Buffering**: Must collect all data before transforming\n- **Code verbosity**: Manual loops for each transformation\n- **Memory usage**: Large intermediate arrays\n- **Composability**: Hard to chain operations\n\nMap/filter enable:\n\n- **Lazy evaluation**: Transform on-demand\n- **Composable**: Chain operations naturally\n- **Memory efficient**: No intermediate collections\n- **Expressive**: Declare intent clearly\n\nReal-world example: Processing logs\n- **Without map/filter**: Collect logs, filter by level, map to objects, transform fields\n- **With map/filter**: `logStream.pipe(Stream.filter(...), Stream.map(...))`\n\n---\n\n## Good Example\n\nThis example demonstrates transforming a stream of raw data through multiple stages.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface RawLogEntry {\n readonly timestamp: string;\n readonly level: string;\n readonly message: string;\n}\n\ninterface ProcessedLog {\n readonly date: Date;\n readonly severity: \"low\" | \"medium\" | \"high\";\n readonly normalizedMessage: string;\n}\n\n// Create a stream of raw log entries\nconst createLogStream = (): Stream.Stream =>\n Stream.fromIterable([\n { timestamp: \"2025-12-17T09:00:00Z\", level: \"DEBUG\", message: \"App starting\" },\n { timestamp: \"2025-12-17T09:01:00Z\", level: \"INFO\", message: \"Connected to DB\" },\n { timestamp: \"2025-12-17T09:02:00Z\", level: \"ERROR\", message: \"Query timeout\" },\n { timestamp: \"2025-12-17T09:03:00Z\", level: \"DEBUG\", message: \"Retry initiated\" },\n { timestamp: \"2025-12-17T09:04:00Z\", level: \"WARN\", message: \"Connection degraded\" },\n { timestamp: \"2025-12-17T09:05:00Z\", level: \"INFO\", message: \"Recovered\" },\n ]);\n\n// Transform: Parse timestamp\nconst parseTimestamp = (entry: RawLogEntry): RawLogEntry => ({\n ...entry,\n timestamp: entry.timestamp, // Already ISO, but could parse here\n});\n\n// Transform: Map log level to severity\nconst mapSeverity = (level: string): \"low\" | \"medium\" | \"high\" => {\n if (level === \"DEBUG\" || level === \"INFO\") return \"low\";\n if (level === \"WARN\") return \"medium\";\n return \"high\";\n};\n\n// Transform: Normalize message\nconst normalizeMessage = (message: string): string =>\n message.toLowerCase().trim();\n\n// Filter: Keep only important logs\nconst isImportant = (entry: RawLogEntry): boolean => {\n return entry.level !== \"DEBUG\";\n};\n\n// Main pipeline\nconst program = Effect.gen(function* () {\n console.log(`\\n[STREAM] Processing log stream with map/filter\\n`);\n\n // Create and transform stream\n const transformedStream = createLogStream().pipe(\n // Filter: Keep only non-debug logs\n Stream.filter((entry) => {\n const important = isImportant(entry);\n console.log(\n `[FILTER] ${entry.level} → ${important ? \"✓ kept\" : \"✗ filtered out\"}`\n );\n return important;\n }),\n\n // Map: Extract date\n Stream.map((entry) => {\n const date = new Date(entry.timestamp);\n console.log(`[MAP-1] Parsed date: ${date.toISOString()}`);\n return { ...entry, parsedDate: date };\n }),\n\n // Map: Normalize and map severity\n Stream.map((entry) => {\n const processed: ProcessedLog = {\n date: entry.parsedDate,\n severity: mapSeverity(entry.level),\n normalizedMessage: normalizeMessage(entry.message),\n };\n console.log(\n `[MAP-2] Transformed: ${entry.level} → ${processed.severity}`\n );\n return processed;\n })\n );\n\n // Collect all transformed logs\n const results = yield* transformedStream.pipe(\n Stream.runCollect\n );\n\n console.log(`\\n[RESULTS]`);\n console.log(` Total logs: ${results.length}`);\n\n Chunk.forEach(results, (log) => {\n console.log(\n ` - [${log.severity.toUpperCase()}] ${log.date.toISOString()}: ${log.normalizedMessage}`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\nOutput shows lazy evaluation and filtering:\n```\n[STREAM] Processing log stream with map/filter\n\n[FILTER] DEBUG → ✗ filtered out\n[FILTER] INFO → ✓ kept\n[MAP-1] Parsed date: 2025-12-17T09:01:00.000Z\n[MAP-2] Transformed: INFO → low\n[FILTER] ERROR → ✓ kept\n[MAP-1] Parsed date: 2025-12-17T09:02:00.000Z\n[MAP-2] Transformed: ERROR → high\n...\n\n[RESULTS]\n Total logs: 5\n - [LOW] 2025-12-17T09:01:00.000Z: connected to db\n - [HIGH] 2025-12-17T09:02:00.000Z: query timeout\n ...\n```\n\n---\n\n## Advanced: Conditional Mapping\n\nApply different transformations based on conditions:\n\n```typescript\nconst conditionalMap = (\n stream: Stream.Stream,\n predicate: (a: A) => boolean,\n mapTrue: (a: A) => B,\n mapFalse: (a: A) => B\n): Stream.Stream =>\n stream.pipe(\n Stream.map((item) =>\n predicate(item) ? mapTrue(item) : mapFalse(item)\n )\n );\n\n// Example: Different handling for high/low severity logs\nconst conditionalLogProcessing = createLogStream().pipe(\n conditionalMap(\n (entry) => entry.level === \"ERROR\" || entry.level === \"WARN\",\n // Transform errors to alerts\n (entry) => ({\n type: \"alert\" as const,\n level: entry.level,\n message: `⚠ ${entry.message}`,\n }),\n // Transform info logs to metrics\n (entry) => ({\n type: \"metric\" as const,\n level: entry.level,\n message: `📊 ${entry.message}`,\n })\n )\n);\n```\n\n---\n\n## Advanced: Stateful Transformations with scan\n\nMaintain state while transforming:\n\n```typescript\ninterface LineMetrics {\n readonly lineNumber: number;\n readonly text: string;\n readonly charCount: number;\n readonly cumulativeChars: number;\n}\n\nconst logStreamWithMetrics = (\n logStream: Stream.Stream\n): Stream.Stream =>\n logStream.pipe(\n Stream.scan(\n { lineNumber: 0, cumulativeChars: 0 },\n (acc, entry) => {\n const charCount = entry.message.length;\n const newAcc = {\n lineNumber: acc.lineNumber + 1,\n cumulativeChars: acc.cumulativeChars + charCount,\n };\n\n return [\n newAcc,\n {\n lineNumber: newAcc.lineNumber,\n text: entry.message,\n charCount,\n cumulativeChars: newAcc.cumulativeChars,\n },\n ];\n }\n )\n );\n\n// Use it\nconst metricsProgram = logStreamWithMetrics(createLogStream()).pipe(\n Stream.runForEach((metric) =>\n Effect.log(\n `Line ${metric.lineNumber}: ${metric.charCount} chars (cumulative: ${metric.cumulativeChars})`\n )\n )\n);\n```\n\n---\n\n## Advanced: Filter with Side Effects\n\nPerform checks with logging during filtering:\n\n```typescript\ninterface FilterMetrics {\n readonly total: number;\n readonly kept: number;\n readonly filtered: number;\n}\n\nconst filterWithMetrics = (\n stream: Stream.Stream,\n predicate: (a: A) => boolean\n): Stream.Stream & { metrics: () => Effect.Effect } => {\n const metrics = { total: 0, kept: 0, filtered: 0 };\n\n const filtered = stream.pipe(\n Stream.tap((item) =>\n Effect.sync(() => {\n metrics.total++;\n })\n ),\n Stream.filter((item) => {\n const result = predicate(item);\n if (result) {\n metrics.kept++;\n } else {\n metrics.filtered++;\n }\n return result;\n })\n );\n\n return Object.assign(filtered, {\n metrics: () => Effect.succeed(metrics),\n });\n};\n```\n\n---\n\n## Advanced: Chained Transformations Pipeline\n\nBuild complex pipelines with multiple stages:\n\n```typescript\ninterface DataPoint {\n readonly id: number;\n readonly value: number;\n readonly timestamp: Date;\n}\n\nconst analyticsPipeline = (\n dataStream: Stream.Stream\n): Stream.Stream<{\n readonly id: number;\n readonly original: number;\n readonly normalized: number;\n readonly category: string;\n}> =>\n dataStream.pipe(\n // Stage 1: Filter out nulls/zeros\n Stream.filter((point) => point.value !== 0),\n\n // Stage 2: Normalize to 0-100 range\n Stream.map((point) => ({\n ...point,\n normalized: Math.min(100, Math.max(0, point.value)),\n })),\n\n // Stage 3: Categorize by value\n Stream.map((point) => ({\n id: point.id,\n original: point.value,\n normalized: point.normalized,\n category:\n point.normalized < 33 ? \"low\" :\n point.normalized < 67 ? \"medium\" :\n \"high\",\n })),\n\n // Stage 4: Filter out only \"low\" for further processing\n Stream.filter((point) => point.category !== \"low\")\n );\n\n// Usage\nconst pipelineExample = analyticsPipeline(\n Stream.fromIterable([\n { id: 1, value: 10, timestamp: new Date() },\n { id: 2, value: 50, timestamp: new Date() },\n { id: 3, value: 0, timestamp: new Date() },\n { id: 4, value: 95, timestamp: new Date() },\n ])\n).pipe(\n Stream.tap((item) =>\n Effect.log(`Processed: ${item.id} → ${item.category}`)\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use map/filter when:**\n\n- Transforming stream elements\n- Selecting subset of elements\n- Chaining multiple operations\n- Building data pipelines\n- Processing without buffering\n- Declarative data transformations\n\n⚠️ **Trade-offs:**\n\n- Each operation adds small overhead\n- Long chains can be hard to debug\n- Can't introspect intermediate values easily\n- Errors propagate through entire pipeline\n\n---\n\n## Map vs Filter vs Fold\n\n| Operator | Input | Output | Use Case |\n| --- | --- | --- | --- |\n| **map** | N elements | N elements (transformed) | Transform each element |\n| **filter** | N elements | M elements (M ≤ N) | Select subset |\n| **fold** | N elements | 1 element | Aggregate all into one |\n| **scan** | N elements | N elements (stateful) | Stateful transformation |\n\n---\n\n## See Also\n\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream basics\n- [Stream Pattern 2: Merge & Combine Streams](./stream-pattern-merge-combine.mdx) - Combining streams\n- [Stream from Iterable](./stream-from-iterable.mdx) - Creating streams\n- [Stream Collect Results](./stream-collect-results.mdx) - Collecting stream output" - }, - { - "id": "stream-pattern-merge-combine", - "title": "Stream Pattern 2: Merge and Combine Multiple Streams", - "description": "Use merge and concat combinators to combine multiple streams, enabling aggregation of data from multiple independent sources.", - "skillLevel": "intermediate", - "useCases": [ - "streams" - ], - "example": "This example demonstrates merging multiple event streams into a unified stream.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface Event {\n readonly source: string;\n readonly type: string;\n readonly data: string;\n readonly timestamp: Date;\n}\n\n// Create independent event streams from different sources\nconst createUserEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"user-service\", type: \"login\", data: \"user-123\", timestamp: new Date(Date.now() + 0) },\n { source: \"user-service\", type: \"logout\", data: \"user-123\", timestamp: new Date(Date.now() + 500) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"500 millis\"))\n );\n\nconst createPaymentEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"payment-service\", type: \"payment-started\", data: \"order-456\", timestamp: new Date(Date.now() + 200) },\n { source: \"payment-service\", type: \"payment-completed\", data: \"order-456\", timestamp: new Date(Date.now() + 800) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"600 millis\"))\n );\n\nconst createAuditEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"audit-log\", type: \"access-granted\", data: \"resource-789\", timestamp: new Date(Date.now() + 100) },\n { source: \"audit-log\", type: \"access-revoked\", data: \"resource-789\", timestamp: new Date(Date.now() + 900) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"800 millis\"))\n );\n\n// Merge streams (interleaved, unordered)\nconst mergedEventStream = (): Stream.Stream => {\n const userStream = createUserEventStream();\n const paymentStream = createPaymentEventStream();\n const auditStream = createAuditEventStream();\n\n return Stream.merge(userStream, paymentStream, auditStream);\n};\n\n// Concat streams (sequential, ordered)\nconst concatenatedEventStream = (): Stream.Stream => {\n return createUserEventStream().pipe(\n Stream.concat(createPaymentEventStream()),\n Stream.concat(createAuditEventStream())\n );\n};\n\n// Main: Compare merge vs concat\nconst program = Effect.gen(function* () {\n console.log(`\\n[MERGE] Interleaved events from multiple sources:\\n`);\n\n // Collect merged stream\n const mergedEvents = yield* mergedEventStream().pipe(\n Stream.runCollect\n );\n\n Chunk.forEach(mergedEvents, (event, idx) => {\n console.log(\n ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`\n );\n });\n\n console.log(`\\n[CONCAT] Sequential events (user → payment → audit):\\n`);\n\n // Collect concatenated stream\n const concatEvents = yield* concatenatedEventStream().pipe(\n Stream.runCollect\n );\n\n Chunk.forEach(concatEvents, (event, idx) => {\n console.log(\n ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\nOutput shows merge interleaving vs concat ordering:\n```\n[MERGE] Interleaved events from multiple sources:\n\n 1. [audit-log] access-granted: resource-789\n 2. [user-service] login: user-123\n 3. [payment-service] payment-started: order-456\n 4. [user-service] logout: user-123\n 5. [payment-service] payment-completed: order-456\n 6. [audit-log] access-revoked: resource-789\n\n[CONCAT] Sequential events (user → payment → audit):\n\n 1. [user-service] login: user-123\n 2. [user-service] logout: user-123\n 3. [payment-service] payment-started: order-456\n 4. [payment-service] payment-completed: order-456\n 5. [audit-log] access-granted: resource-789\n 6. [audit-log] access-revoked: resource-789\n```\n\n---", - "antiPattern": "", - "explanation": "Multi-source data processing without merge/concat creates issues:\n\n- **Complex coordination**: Manual loop over multiple sources\n- **Hard to aggregate**: Collecting from different sources is verbose\n- **Ordering confusion**: Sequential vs. parallel unclear\n- **Resource management**: Multiple independent consumers\n\nMerge/concat enable:\n\n- **Simple composition**: Combine streams naturally\n- **Semantic clarity**: Merge = parallel, concat = sequential\n- **Aggregation**: Single consumer for multiple sources\n- **Scalability**: Add sources without refactoring\n\nReal-world example: Aggregating user events\n- **Without merge**: Poll user service, poll event log, poll notifications separately\n- **With merge**: `Stream.merge(userStream, eventStream, notificationStream)`\n\n---", - "content": "## Guideline\n\nCombine multiple streams using:\n\n- **merge**: Interleave elements from multiple streams (unordered)\n- **concat**: Chain streams sequentially (ordered, waits for first to complete)\n- **mergeAll**: Merge collection of streams\n- **zip**: Combine corresponding elements from multiple streams\n\nPattern: `Stream.merge(stream1, stream2)` or `stream1.pipe(Stream.concat(stream2))`\n\n---\n\n## Rationale\n\nMulti-source data processing without merge/concat creates issues:\n\n- **Complex coordination**: Manual loop over multiple sources\n- **Hard to aggregate**: Collecting from different sources is verbose\n- **Ordering confusion**: Sequential vs. parallel unclear\n- **Resource management**: Multiple independent consumers\n\nMerge/concat enable:\n\n- **Simple composition**: Combine streams naturally\n- **Semantic clarity**: Merge = parallel, concat = sequential\n- **Aggregation**: Single consumer for multiple sources\n- **Scalability**: Add sources without refactoring\n\nReal-world example: Aggregating user events\n- **Without merge**: Poll user service, poll event log, poll notifications separately\n- **With merge**: `Stream.merge(userStream, eventStream, notificationStream)`\n\n---\n\n## Good Example\n\nThis example demonstrates merging multiple event streams into a unified stream.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface Event {\n readonly source: string;\n readonly type: string;\n readonly data: string;\n readonly timestamp: Date;\n}\n\n// Create independent event streams from different sources\nconst createUserEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"user-service\", type: \"login\", data: \"user-123\", timestamp: new Date(Date.now() + 0) },\n { source: \"user-service\", type: \"logout\", data: \"user-123\", timestamp: new Date(Date.now() + 500) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"500 millis\"))\n );\n\nconst createPaymentEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"payment-service\", type: \"payment-started\", data: \"order-456\", timestamp: new Date(Date.now() + 200) },\n { source: \"payment-service\", type: \"payment-completed\", data: \"order-456\", timestamp: new Date(Date.now() + 800) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"600 millis\"))\n );\n\nconst createAuditEventStream = (): Stream.Stream =>\n Stream.fromIterable([\n { source: \"audit-log\", type: \"access-granted\", data: \"resource-789\", timestamp: new Date(Date.now() + 100) },\n { source: \"audit-log\", type: \"access-revoked\", data: \"resource-789\", timestamp: new Date(Date.now() + 900) },\n ]).pipe(\n Stream.tap(() => Effect.sleep(\"800 millis\"))\n );\n\n// Merge streams (interleaved, unordered)\nconst mergedEventStream = (): Stream.Stream => {\n const userStream = createUserEventStream();\n const paymentStream = createPaymentEventStream();\n const auditStream = createAuditEventStream();\n\n return Stream.merge(userStream, paymentStream, auditStream);\n};\n\n// Concat streams (sequential, ordered)\nconst concatenatedEventStream = (): Stream.Stream => {\n return createUserEventStream().pipe(\n Stream.concat(createPaymentEventStream()),\n Stream.concat(createAuditEventStream())\n );\n};\n\n// Main: Compare merge vs concat\nconst program = Effect.gen(function* () {\n console.log(`\\n[MERGE] Interleaved events from multiple sources:\\n`);\n\n // Collect merged stream\n const mergedEvents = yield* mergedEventStream().pipe(\n Stream.runCollect\n );\n\n Chunk.forEach(mergedEvents, (event, idx) => {\n console.log(\n ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`\n );\n });\n\n console.log(`\\n[CONCAT] Sequential events (user → payment → audit):\\n`);\n\n // Collect concatenated stream\n const concatEvents = yield* concatenatedEventStream().pipe(\n Stream.runCollect\n );\n\n Chunk.forEach(concatEvents, (event, idx) => {\n console.log(\n ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}`\n );\n });\n});\n\nEffect.runPromise(program);\n```\n\nOutput shows merge interleaving vs concat ordering:\n```\n[MERGE] Interleaved events from multiple sources:\n\n 1. [audit-log] access-granted: resource-789\n 2. [user-service] login: user-123\n 3. [payment-service] payment-started: order-456\n 4. [user-service] logout: user-123\n 5. [payment-service] payment-completed: order-456\n 6. [audit-log] access-revoked: resource-789\n\n[CONCAT] Sequential events (user → payment → audit):\n\n 1. [user-service] login: user-123\n 2. [user-service] logout: user-123\n 3. [payment-service] payment-started: order-456\n 4. [payment-service] payment-completed: order-456\n 5. [audit-log] access-granted: resource-789\n 6. [audit-log] access-revoked: resource-789\n```\n\n---\n\n## Advanced: Merging with Prioritization\n\nHandle priority streams differently:\n\n```typescript\ninterface PrioritizedEvent extends Event {\n readonly priority: number; // 1=low, 5=high\n}\n\nconst priorityMergeStream = (\n highPriorityStream: Stream.Stream,\n normalPriorityStream: Stream.Stream\n): Stream.Stream =>\n Stream.mergeAll([\n // Take high priority first, then normal\n highPriorityStream,\n normalPriorityStream,\n ]).pipe(\n // Sort by priority (though stream nature means some interleaving)\n Stream.tap((event) =>\n Effect.log(\n `Event [priority=${event.priority}] ${event.type}`\n )\n )\n );\n```\n\n---\n\n## Advanced: Zip/Combine Corresponding Elements\n\nWait for matching elements from multiple streams:\n\n```typescript\ninterface RequestEvent {\n readonly requestId: string;\n readonly timestamp: Date;\n}\n\ninterface ResponseEvent {\n readonly requestId: string;\n readonly duration: number;\n readonly timestamp: Date;\n}\n\ninterface RequestResponse {\n readonly requestId: string;\n readonly duration: number;\n}\n\nconst zipRequestResponse = (\n requestStream: Stream.Stream,\n responseStream: Stream.Stream\n): Stream.Stream =>\n requestStream.pipe(\n Stream.zip(responseStream),\n Stream.map(([request, response]) => ({\n requestId: request.requestId,\n duration: response.duration,\n }))\n );\n\n// Usage: Correlate requests with responses\nconst correlatedStream = zipRequestResponse(\n Stream.fromIterable([\n { requestId: \"req-1\", timestamp: new Date() },\n { requestId: \"req-2\", timestamp: new Date() },\n ]),\n Stream.fromIterable([\n { requestId: \"req-1\", duration: 100, timestamp: new Date() },\n { requestId: \"req-2\", duration: 150, timestamp: new Date() },\n ])\n).pipe(\n Stream.tap((item) =>\n Effect.log(\n `Request ${item.requestId} took ${item.duration}ms`\n )\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## Advanced: Merge with Error Handling\n\nHandle errors from merged streams:\n\n```typescript\nconst mergeWithErrorHandling = (\n streams: Stream.Stream[]\n): Stream.Stream<\n { tag: \"success\"; value: A } | { tag: \"error\"; error: Error }\n> =>\n Stream.mergeAll(\n streams.map((stream) =>\n stream.pipe(\n Stream.map((value) => ({ tag: \"success\" as const, value })),\n Stream.catchAll((error) =>\n Stream.succeed({\n tag: \"error\" as const,\n error: error as Error,\n })\n )\n )\n )\n );\n\n// Usage\nconst resilientMerge = mergeWithErrorHandling([\n createUserEventStream(),\n createPaymentEventStream(),\n createAuditEventStream(),\n]).pipe(\n Stream.tap((item) => {\n if (item.tag === \"error\") {\n return Effect.log(`Error from stream: ${item.error.message}`);\n } else {\n return Effect.log(`Event: ${item.value.type}`);\n }\n }),\n Stream.runDrain\n);\n```\n\n---\n\n## Advanced: Round-Robin Merge\n\nFairly interleave elements from multiple streams:\n\n```typescript\nconst roundRobinMerge = (\n streams: Stream.Stream[]\n): Stream.Stream => {\n const queues = streams.map(() => Queue.bounded(1));\n\n // Forward each stream to its queue\n const forwarders = streams.map((stream, idx) =>\n stream.pipe(\n Stream.runForEach((item) => Queue.offer(queues[idx], item))\n )\n );\n\n // Consume from queues in round-robin fashion\n return Stream.fromEffect(\n Effect.gen(function* () {\n yield* Effect.all(forwarders.map((f) => Effect.fork(f)));\n\n let currentQueue = 0;\n\n return Stream.repeatEffect(\n Effect.gen(function* () {\n // Try queues in round-robin order\n for (let i = 0; i < queues.length; i++) {\n const queueIdx = (currentQueue + i) % queues.length;\n const item = yield* Queue.poll(queues[queueIdx]);\n\n if (item._tag === \"Some\") {\n currentQueue = queueIdx;\n return item.value;\n }\n }\n\n // Wait and retry\n yield* Effect.sleep(\"10 millis\");\n return yield* roundRobinMerge(streams).pipe(\n Stream.take(1),\n Stream.runCollect\n );\n })\n );\n })\n ).pipe(Stream.flatten);\n};\n```\n\n---\n\n## Advanced: Merge with Deduplication\n\nRemove duplicate events from merged streams:\n\n```typescript\nconst mergeDeduped = (\n streams: Stream.Stream[]\n): Stream.Stream => {\n const seen = new Set();\n\n return Stream.mergeAll(streams).pipe(\n Stream.filter((item) => {\n if (seen.has(item.id)) {\n return false; // Duplicate, filter out\n }\n seen.add(item.id);\n return true;\n })\n );\n};\n\n// Usage: Merge event streams, remove duplicates\nconst dedupedEvents = mergeDeduped([\n createUserEventStream(),\n createPaymentEventStream(),\n createAuditEventStream(),\n]).pipe(\n Stream.tap((event) =>\n Effect.log(`Unique event: ${event.type}`)\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use merge/concat when:**\n\n- Combining multiple independent streams\n- Aggregating from different sources\n- Multi-source data pipelines\n- Event correlation and joining\n- Fan-in patterns (many→one)\n\n✅ **Merge** when: Parallel, interleaved order acceptable\n\n✅ **Concat** when: Sequential, specific order needed\n\n⚠️ **Trade-offs:**\n\n- Merge overhead grows with stream count\n- Can't resume individual failed streams\n- Backpressure affects all sources\n- Ordering unclear with merge\n\n---\n\n## Merge vs Concat vs Zip\n\n| Operator | Behavior | Use Case |\n| --- | --- | --- |\n| **merge** | Parallel, interleaved | Fan-in, parallel sources |\n| **concat** | Sequential, waits for first | Chaining ordered sequences |\n| **zip** | Wait for corresponding elements | Correlating matched pairs |\n| **mergeAll** | Merge collection of streams | Dynamic number of sources |\n\n---\n\n## See Also\n\n- [Stream Pattern 1: Map & Filter Transformations](./stream-pattern-map-filter-transformations.mdx) - Stream transformations\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream basics\n- [Stream from Iterable](./stream-from-iterable.mdx) - Creating streams\n- [Stream Collect Results](./stream-collect-results.mdx) - Collecting stream output" - }, - { - "id": "stream-pattern-backpressure-control", - "title": "Stream Pattern 3: Control Backpressure in Streams", - "description": "Use backpressure control to manage flow between fast producers and slow consumers, preventing memory exhaustion and resource overflow.", - "skillLevel": "intermediate", - "useCases": [ - "streams" - ], - "example": "This example demonstrates managing backpressure when consuming events at different rates.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface DataPoint {\n readonly id: number;\n readonly value: number;\n}\n\n// Fast producer: generates 100 items per second\nconst fastProducer = (): Stream.Stream =>\n Stream.fromIterable(Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.random() }))).pipe(\n Stream.tap(() => Effect.sleep(\"10 millis\")) // 10ms per item = 100/sec\n );\n\n// Slow consumer: processes 10 items per second\nconst slowConsumer = (item: DataPoint): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\"); // 100ms per item = 10/sec\n });\n\n// Without backpressure (DANGEROUS - queue grows unbounded)\nconst unbufferedStream = (): Stream.Stream =>\n fastProducer().pipe(\n Stream.tap((item) =>\n Effect.log(`[UNBUFFERED] Produced item ${item.id}`)\n )\n );\n\n// With bounded buffer (backpressure kicks in)\nconst bufferedStream = (bufferSize: number): Stream.Stream =>\n fastProducer().pipe(\n // Buffer at most 10 items; if full, producer waits\n Stream.buffer(bufferSize),\n Stream.tap((item) =>\n Effect.log(`[BUFFERED] Consumed item ${item.id}`)\n )\n );\n\n// With throttling (rate limit emission)\nconst throttledStream = (): Stream.Stream =>\n fastProducer().pipe(\n // Emit at most 1 item per 50ms (20/sec)\n Stream.throttle(1, \"50 millis\"),\n Stream.tap((item) =>\n Effect.log(`[THROTTLED] Item ${item.id}`)\n )\n );\n\n// Main: compare approaches\nconst program = Effect.gen(function* () {\n console.log(`\\n[START] Demonstrating backpressure management\\n`);\n\n // Test buffered approach\n console.log(`[TEST 1] Buffered stream (buffer size 5):\\n`);\n\n const startBuffer = Date.now();\n\n yield* bufferedStream(5).pipe(\n Stream.take(20), // Take only 20 items\n Stream.runForEach(slowConsumer)\n );\n\n const bufferTime = Date.now() - startBuffer;\n console.log(`\\n[RESULT] Buffered approach took ${bufferTime}ms\\n`);\n\n // Test throttled approach\n console.log(`[TEST 2] Throttled stream (1 item per 50ms):\\n`);\n\n const startThrottle = Date.now();\n\n yield* throttledStream().pipe(\n Stream.take(20),\n Stream.runForEach(slowConsumer)\n );\n\n const throttleTime = Date.now() - startThrottle;\n console.log(`\\n[RESULT] Throttled approach took ${throttleTime}ms\\n`);\n\n // Summary\n console.log(`[SUMMARY]`);\n console.log(` Without backpressure control:`);\n console.log(` - Queue would grow to 100 items (memory risk)`);\n console.log(` - Producer/consumer operate independently`);\n console.log(` With buffering:`);\n console.log(` - Queue bounded to 5 items (safe)`);\n console.log(` - Producer waits when buffer full`);\n console.log(` With throttling:`);\n console.log(` - Production rate limited to 20/sec`);\n console.log(` - Smooth controlled flow`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Without backpressure management, mismatched producer/consumer speeds cause:\n\n- **Memory exhaustion**: Producer faster than consumer → queue grows unbounded\n- **Garbage collection pauses**: Large buffers cause GC pressure\n- **Resource leaks**: Open connections/file handles accumulate\n- **Cascade failures**: One slow consumer blocks entire pipeline\n\nBackpressure enable:\n\n- **Memory safety**: Bounded buffers prevent overflow\n- **Resource efficiency**: Consumers pace producers naturally\n- **Performance**: Tuning buffer sizes improves throughput\n- **Observability**: Monitor backpressure as health indicator\n\nReal-world example: Reading large file vs. writing to database\n- **No backpressure**: Read entire file into memory, write slowly → memory exhaustion\n- **With backpressure**: Read 1000 lines, wait for database, read next batch\n\n---", - "content": "## Guideline\n\nBackpressure is flow control: slow consumer tells fast producer to slow down.\n\nTechniques:\n- **Buffering**: Store items temporarily (limited queue)\n- **Throttling**: Rate limit item emission\n- **Chunking**: Process in fixed-size batches\n- **Debouncing**: Skip rapid duplicates\n\nPattern: `stream.pipe(Stream.throttle(...), Stream.buffer(...))`\n\n---\n\n## Rationale\n\nWithout backpressure management, mismatched producer/consumer speeds cause:\n\n- **Memory exhaustion**: Producer faster than consumer → queue grows unbounded\n- **Garbage collection pauses**: Large buffers cause GC pressure\n- **Resource leaks**: Open connections/file handles accumulate\n- **Cascade failures**: One slow consumer blocks entire pipeline\n\nBackpressure enable:\n\n- **Memory safety**: Bounded buffers prevent overflow\n- **Resource efficiency**: Consumers pace producers naturally\n- **Performance**: Tuning buffer sizes improves throughput\n- **Observability**: Monitor backpressure as health indicator\n\nReal-world example: Reading large file vs. writing to database\n- **No backpressure**: Read entire file into memory, write slowly → memory exhaustion\n- **With backpressure**: Read 1000 lines, wait for database, read next batch\n\n---\n\n## Good Example\n\nThis example demonstrates managing backpressure when consuming events at different rates.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface DataPoint {\n readonly id: number;\n readonly value: number;\n}\n\n// Fast producer: generates 100 items per second\nconst fastProducer = (): Stream.Stream =>\n Stream.fromIterable(Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.random() }))).pipe(\n Stream.tap(() => Effect.sleep(\"10 millis\")) // 10ms per item = 100/sec\n );\n\n// Slow consumer: processes 10 items per second\nconst slowConsumer = (item: DataPoint): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\"); // 100ms per item = 10/sec\n });\n\n// Without backpressure (DANGEROUS - queue grows unbounded)\nconst unbufferedStream = (): Stream.Stream =>\n fastProducer().pipe(\n Stream.tap((item) =>\n Effect.log(`[UNBUFFERED] Produced item ${item.id}`)\n )\n );\n\n// With bounded buffer (backpressure kicks in)\nconst bufferedStream = (bufferSize: number): Stream.Stream =>\n fastProducer().pipe(\n // Buffer at most 10 items; if full, producer waits\n Stream.buffer(bufferSize),\n Stream.tap((item) =>\n Effect.log(`[BUFFERED] Consumed item ${item.id}`)\n )\n );\n\n// With throttling (rate limit emission)\nconst throttledStream = (): Stream.Stream =>\n fastProducer().pipe(\n // Emit at most 1 item per 50ms (20/sec)\n Stream.throttle(1, \"50 millis\"),\n Stream.tap((item) =>\n Effect.log(`[THROTTLED] Item ${item.id}`)\n )\n );\n\n// Main: compare approaches\nconst program = Effect.gen(function* () {\n console.log(`\\n[START] Demonstrating backpressure management\\n`);\n\n // Test buffered approach\n console.log(`[TEST 1] Buffered stream (buffer size 5):\\n`);\n\n const startBuffer = Date.now();\n\n yield* bufferedStream(5).pipe(\n Stream.take(20), // Take only 20 items\n Stream.runForEach(slowConsumer)\n );\n\n const bufferTime = Date.now() - startBuffer;\n console.log(`\\n[RESULT] Buffered approach took ${bufferTime}ms\\n`);\n\n // Test throttled approach\n console.log(`[TEST 2] Throttled stream (1 item per 50ms):\\n`);\n\n const startThrottle = Date.now();\n\n yield* throttledStream().pipe(\n Stream.take(20),\n Stream.runForEach(slowConsumer)\n );\n\n const throttleTime = Date.now() - startThrottle;\n console.log(`\\n[RESULT] Throttled approach took ${throttleTime}ms\\n`);\n\n // Summary\n console.log(`[SUMMARY]`);\n console.log(` Without backpressure control:`);\n console.log(` - Queue would grow to 100 items (memory risk)`);\n console.log(` - Producer/consumer operate independently`);\n console.log(` With buffering:`);\n console.log(` - Queue bounded to 5 items (safe)`);\n console.log(` - Producer waits when buffer full`);\n console.log(` With throttling:`);\n console.log(` - Production rate limited to 20/sec`);\n console.log(` - Smooth controlled flow`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Adaptive Backpressure\n\nAdjust buffering based on consumption rate:\n\n```typescript\ninterface BackpressureMetrics {\n readonly bufferSize: number;\n readonly avgWaitTime: number;\n readonly drainRate: number;\n}\n\nconst adaptiveBuffer = (\n stream: Stream.Stream,\n initialBufferSize: number\n): Stream.Stream =>\n Stream.gen(function* () {\n const metrics = {\n bufferSize: initialBufferSize,\n itemsProcessed: 0,\n totalWaitTime: 0,\n };\n\n const adaptiveStream = stream.pipe(\n Stream.buffer(metrics.bufferSize),\n Stream.tap((item) => {\n metrics.itemsProcessed++;\n })\n );\n\n // Monitor and adjust every 1000 items\n const monitor = Effect.gen(function* () {\n while (true) {\n yield* Effect.sleep(\"1 second\");\n\n const avgWait = metrics.totalWaitTime / Math.max(1, metrics.itemsProcessed);\n const drainRate = metrics.itemsProcessed / 1000;\n\n yield* Effect.log(\n `[ADAPTIVE] Buffer: ${metrics.bufferSize}, Wait: ${avgWait.toFixed(1)}ms, Rate: ${drainRate.toFixed(1)} items/sec`\n );\n\n // Increase buffer if high wait times\n if (avgWait > 100 && metrics.bufferSize < 100) {\n metrics.bufferSize *= 1.5;\n yield* Effect.log(\n `[ADAPTIVE] Increased buffer to ${metrics.bufferSize}`\n );\n }\n\n // Decrease buffer if low wait times\n if (avgWait < 10 && metrics.bufferSize > initialBufferSize) {\n metrics.bufferSize /= 1.5;\n yield* Effect.log(\n `[ADAPTIVE] Decreased buffer to ${metrics.bufferSize}`\n );\n }\n\n metrics.itemsProcessed = 0;\n metrics.totalWaitTime = 0;\n }\n });\n\n yield* Effect.fork(monitor);\n yield* adaptiveStream;\n });\n```\n\n---\n\n## Advanced: Chunk Processing with Backpressure\n\nProcess items in fixed-size chunks while managing backpressure:\n\n```typescript\nconst chunkedProcessing = (\n stream: Stream.Stream,\n chunkSize: number,\n processChunk: (chunk: Chunk.Chunk) => Effect.Effect\n): Stream.Stream =>\n stream.pipe(\n // Collect into chunks\n Stream.chunks,\n // Filter to desired size (drop smaller end chunks or use sliding)\n Stream.filter((chunk) => Chunk.size(chunk) === chunkSize),\n // Process each chunk\n Stream.mapEffect(processChunk),\n // Buffer to prevent backlog\n Stream.buffer(5)\n );\n\n// Usage: Process log lines in 100-item batches\nconst batchedLogProcessing = chunkedProcessing(\n Stream.fromIterable(\n Array.from({ length: 1000 }, (_, i) => `log-line-${i}`)\n ),\n 100,\n (chunk) =>\n Effect.gen(function* () {\n const count = Chunk.size(chunk);\n yield* Effect.log(\n `Processing batch of ${count} items`\n );\n yield* Effect.sleep(\"500 millis\");\n return count;\n })\n).pipe(\n Stream.tap((count) =>\n Effect.log(`Processed ${count} items`)\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## Advanced: Debouncing Rapid Events\n\nSkip duplicates or rapid events:\n\n```typescript\ninterface Event {\n readonly id: string;\n readonly timestamp: Date;\n}\n\nconst debounceStream = (\n stream: Stream.Stream,\n delayMs: number\n): Stream.Stream => {\n const lastEmit = new Map();\n\n return stream.pipe(\n Stream.filter((item) => {\n const now = Date.now();\n const lastTime = lastEmit.get(item.id) ?? 0;\n\n if (now - lastTime > delayMs) {\n lastEmit.set(item.id, now);\n return true; // Emit\n }\n\n return false; // Skip (too recent)\n })\n );\n};\n\n// Usage: Debounce rapid user activity events\nconst debouncedEvents = debounceStream(\n Stream.fromIterable([\n { id: \"user-123\", timestamp: new Date() },\n { id: \"user-123\", timestamp: new Date() }, // Skipped\n { id: \"user-123\", timestamp: new Date() }, // Skipped\n { id: \"user-456\", timestamp: new Date() },\n { id: \"user-456\", timestamp: new Date() }, // Skipped\n ]),\n 100 // Wait 100ms between events for same user\n).pipe(\n Stream.tap((event) =>\n Effect.log(`Debounced: ${event.id}`)\n ),\n Stream.runDrain\n);\n```\n\n---\n\n## Advanced: Multi-Level Backpressure Strategy\n\nCombine multiple backpressure techniques:\n\n```typescript\ninterface BackpressureStrategy {\n readonly bufferSize: number;\n readonly throttlePerSec: number;\n readonly chunkSize: number;\n readonly debounceMs: number;\n}\n\nconst applyBackpressureStrategy = (\n stream: Stream.Stream,\n strategy: BackpressureStrategy\n): Stream.Stream =>\n stream.pipe(\n // Level 1: Debounce rapid items\n Stream.filter(() => true), // Add debounce logic here\n\n // Level 2: Throttle emission rate\n Stream.throttle(\n strategy.throttlePerSec,\n \"1 second\"\n ),\n\n // Level 3: Buffer with bounded queue\n Stream.buffer(strategy.bufferSize),\n\n // Level 4: Chunk for batch processing\n Stream.chunkN(strategy.chunkSize),\n\n // Monitor backpressure health\n Stream.tap((chunk) =>\n Effect.log(\n `[BACKPRESSURE] Chunk size: ${Chunk.size(chunk)}`\n )\n )\n );\n\n// Conservative strategy for high-volume sources\nconst conservativeStrategy: BackpressureStrategy = {\n bufferSize: 5,\n throttlePerSec: 100,\n chunkSize: 10,\n debounceMs: 50,\n};\n\n// Aggressive strategy for normal-volume sources\nconst aggressiveStrategy: BackpressureStrategy = {\n bufferSize: 100,\n throttlePerSec: 10000,\n chunkSize: 1000,\n debounceMs: 0,\n};\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use backpressure control when:**\n\n- Producer faster than consumer\n- Memory constraints (prevent unbounded growth)\n- High-frequency data sources (sensors, market data)\n- Network streams (files, APIs)\n- Rate limiting requirements\n\n⚠️ **Trade-offs:**\n\n- Buffering adds latency\n- Throttling reduces throughput\n- Tuning buffers requires experimentation\n- Monitoring overhead\n\n---\n\n## Backpressure Techniques Comparison\n\n| Technique | Mechanism | Latency Impact | Memory Impact | Use Case |\n| --- | --- | --- | --- | --- |\n| **Buffer** | Bounded queue | Medium | Controlled | Normal flow control |\n| **Throttle** | Rate limiting | High | Low | Protecting downstream |\n| **Chunk** | Batch processing | Low-Medium | Low | Bulk operations |\n| **Debounce** | Skip rapid | Low | Very low | Duplicate reduction |\n\n---\n\n## See Also\n\n- [Stream Pattern 1: Map & Filter Transformations](./stream-pattern-map-filter-transformations.mdx) - Stream transformations\n- [Concurrency Pattern 2: Rate Limit with Semaphore](./concurrency-pattern-rate-limit-with-semaphore.mdx) - Rate limiting\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream basics\n- [Stream Collect Results](./stream-collect-results.mdx) - Collecting stream output" - }, - { - "id": "stream-pattern-stateful-operations", - "title": "Stream Pattern 4: Stateful Operations with Scan and Fold", - "description": "Use scan for stateful element-by-element processing and fold for final aggregation, enabling complex stream analytics without buffering entire stream.", - "skillLevel": "intermediate", - "useCases": [ - "streams" - ], - "example": "This example demonstrates maintaining statistics across a stream of measurements.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface Measurement {\n readonly id: number;\n readonly value: number;\n readonly timestamp: Date;\n}\n\ninterface RunningStats {\n readonly count: number;\n readonly sum: number;\n readonly min: number;\n readonly max: number;\n readonly average: number;\n readonly variance: number;\n readonly lastValue: number;\n}\n\n// Create stream of measurements\nconst createMeasurementStream = (): Stream.Stream =>\n Stream.fromIterable([\n { id: 1, value: 10, timestamp: new Date() },\n { id: 2, value: 20, timestamp: new Date() },\n { id: 3, value: 15, timestamp: new Date() },\n { id: 4, value: 25, timestamp: new Date() },\n { id: 5, value: 30, timestamp: new Date() },\n { id: 6, value: 22, timestamp: new Date() },\n ]);\n\n// Initial statistics state\nconst initialStats: RunningStats = {\n count: 0,\n sum: 0,\n min: Infinity,\n max: -Infinity,\n average: 0,\n variance: 0,\n lastValue: 0,\n};\n\n// Reducer: update stats for each measurement\nconst updateStats = (\n stats: RunningStats,\n measurement: Measurement\n): RunningStats => {\n const newCount = stats.count + 1;\n const newSum = stats.sum + measurement.value;\n const newAverage = newSum / newCount;\n\n // Calculate variance incrementally\n const delta = measurement.value - stats.average;\n const delta2 = measurement.value - newAverage;\n const newVariance = stats.variance + delta * delta2;\n\n return {\n count: newCount,\n sum: newSum,\n min: Math.min(stats.min, measurement.value),\n max: Math.max(stats.max, measurement.value),\n average: newAverage,\n variance: newVariance / newCount,\n lastValue: measurement.value,\n };\n};\n\n// Main: demonstrate scan with statistics\nconst program = Effect.gen(function* () {\n console.log(`\\n[SCAN] Running statistics stream:\\n`);\n\n // Use scan to emit intermediate statistics\n const statsStream = createMeasurementStream().pipe(\n Stream.scan(initialStats, (stats, measurement) => {\n const newStats = updateStats(stats, measurement);\n\n console.log(\n `[MEASUREMENT ${measurement.id}] Value: ${measurement.value}`\n );\n console.log(\n ` Count: ${newStats.count}, Avg: ${newStats.average.toFixed(2)}, ` +\n `Min: ${newStats.min}, Max: ${newStats.max}, ` +\n `Variance: ${newStats.variance.toFixed(2)}`\n );\n\n return newStats;\n })\n );\n\n // Collect all intermediate stats\n const allStats = yield* statsStream.pipe(Stream.runCollect);\n\n // Final statistics\n const finalStats = Chunk.last(allStats);\n\n if (finalStats._tag === \"Some\") {\n console.log(`\\n[FINAL STATISTICS]`);\n console.log(` Total measurements: ${finalStats.value.count}`);\n console.log(` Average: ${finalStats.value.average.toFixed(2)}`);\n console.log(` Min: ${finalStats.value.min}`);\n console.log(` Max: ${finalStats.value.max}`);\n console.log(\n ` Std Dev: ${Math.sqrt(finalStats.value.variance).toFixed(2)}`\n );\n }\n\n // Compare with fold (emit only final result)\n console.log(`\\n[FOLD] Final statistics only:\\n`);\n\n const finalResult = yield* createMeasurementStream().pipe(\n Stream.fold(initialStats, updateStats),\n Stream.tap((stats) =>\n Effect.log(`Final: Count=${stats.count}, Avg=${stats.average.toFixed(2)}`)\n )\n );\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Processing streams without scan/fold creates issues:\n\n- **Manual state tracking**: Ref or mutable variables outside stream\n- **Lost context**: Hard to correlate intermediate values\n- **Error-prone**: Easy to forget state updates\n- **Testing difficulty**: State spread across code\n\nScan/fold enable:\n\n- **Declarative state**: State threaded through stream\n- **Intermediate values**: Emit state at each step (scan)\n- **Type-safe**: Accumulator type guaranteed\n- **Composable**: Chain stateful operations\n\nReal-world example: Running average of metrics\n- **Without scan**: Track count and sum manually, calculate average, emit\n- **With scan**: `stream.pipe(Stream.scan(initialState, updateAverage))`\n\n---", - "content": "## Guideline\n\nStateful stream operations:\n\n- **scan**: Apply function with accumulator, emit intermediate states\n- **fold**: Apply function with accumulator, emit only final result\n- **reduce**: Like fold but requires non-empty stream\n\nPattern: `stream.pipe(Stream.scan(initialState, reducer))` or `Stream.fold(initialState, reducer)`\n\n---\n\n## Rationale\n\nProcessing streams without scan/fold creates issues:\n\n- **Manual state tracking**: Ref or mutable variables outside stream\n- **Lost context**: Hard to correlate intermediate values\n- **Error-prone**: Easy to forget state updates\n- **Testing difficulty**: State spread across code\n\nScan/fold enable:\n\n- **Declarative state**: State threaded through stream\n- **Intermediate values**: Emit state at each step (scan)\n- **Type-safe**: Accumulator type guaranteed\n- **Composable**: Chain stateful operations\n\nReal-world example: Running average of metrics\n- **Without scan**: Track count and sum manually, calculate average, emit\n- **With scan**: `stream.pipe(Stream.scan(initialState, updateAverage))`\n\n---\n\n## Good Example\n\nThis example demonstrates maintaining statistics across a stream of measurements.\n\n```typescript\nimport { Stream, Effect, Chunk } from \"effect\";\n\ninterface Measurement {\n readonly id: number;\n readonly value: number;\n readonly timestamp: Date;\n}\n\ninterface RunningStats {\n readonly count: number;\n readonly sum: number;\n readonly min: number;\n readonly max: number;\n readonly average: number;\n readonly variance: number;\n readonly lastValue: number;\n}\n\n// Create stream of measurements\nconst createMeasurementStream = (): Stream.Stream =>\n Stream.fromIterable([\n { id: 1, value: 10, timestamp: new Date() },\n { id: 2, value: 20, timestamp: new Date() },\n { id: 3, value: 15, timestamp: new Date() },\n { id: 4, value: 25, timestamp: new Date() },\n { id: 5, value: 30, timestamp: new Date() },\n { id: 6, value: 22, timestamp: new Date() },\n ]);\n\n// Initial statistics state\nconst initialStats: RunningStats = {\n count: 0,\n sum: 0,\n min: Infinity,\n max: -Infinity,\n average: 0,\n variance: 0,\n lastValue: 0,\n};\n\n// Reducer: update stats for each measurement\nconst updateStats = (\n stats: RunningStats,\n measurement: Measurement\n): RunningStats => {\n const newCount = stats.count + 1;\n const newSum = stats.sum + measurement.value;\n const newAverage = newSum / newCount;\n\n // Calculate variance incrementally\n const delta = measurement.value - stats.average;\n const delta2 = measurement.value - newAverage;\n const newVariance = stats.variance + delta * delta2;\n\n return {\n count: newCount,\n sum: newSum,\n min: Math.min(stats.min, measurement.value),\n max: Math.max(stats.max, measurement.value),\n average: newAverage,\n variance: newVariance / newCount,\n lastValue: measurement.value,\n };\n};\n\n// Main: demonstrate scan with statistics\nconst program = Effect.gen(function* () {\n console.log(`\\n[SCAN] Running statistics stream:\\n`);\n\n // Use scan to emit intermediate statistics\n const statsStream = createMeasurementStream().pipe(\n Stream.scan(initialStats, (stats, measurement) => {\n const newStats = updateStats(stats, measurement);\n\n console.log(\n `[MEASUREMENT ${measurement.id}] Value: ${measurement.value}`\n );\n console.log(\n ` Count: ${newStats.count}, Avg: ${newStats.average.toFixed(2)}, ` +\n `Min: ${newStats.min}, Max: ${newStats.max}, ` +\n `Variance: ${newStats.variance.toFixed(2)}`\n );\n\n return newStats;\n })\n );\n\n // Collect all intermediate stats\n const allStats = yield* statsStream.pipe(Stream.runCollect);\n\n // Final statistics\n const finalStats = Chunk.last(allStats);\n\n if (finalStats._tag === \"Some\") {\n console.log(`\\n[FINAL STATISTICS]`);\n console.log(` Total measurements: ${finalStats.value.count}`);\n console.log(` Average: ${finalStats.value.average.toFixed(2)}`);\n console.log(` Min: ${finalStats.value.min}`);\n console.log(` Max: ${finalStats.value.max}`);\n console.log(\n ` Std Dev: ${Math.sqrt(finalStats.value.variance).toFixed(2)}`\n );\n }\n\n // Compare with fold (emit only final result)\n console.log(`\\n[FOLD] Final statistics only:\\n`);\n\n const finalResult = yield* createMeasurementStream().pipe(\n Stream.fold(initialStats, updateStats),\n Stream.tap((stats) =>\n Effect.log(`Final: Count=${stats.count}, Avg=${stats.average.toFixed(2)}`)\n )\n );\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Multi-Stage State Machine\n\nUse scan to implement state machine across stream:\n\n```typescript\nenum State {\n Idle = \"idle\",\n Processing = \"processing\",\n Complete = \"complete\",\n}\n\ninterface WorkItem {\n readonly id: number;\n readonly status: \"pending\" | \"done\" | \"error\";\n}\n\ninterface WorkState {\n readonly state: State;\n readonly itemsProcessed: number;\n readonly itemsError: number;\n readonly currentBatch: number;\n}\n\nconst stateTransition = (\n workState: WorkState,\n item: WorkItem\n): WorkState => {\n // State machine logic\n if (workState.state === \"idle\" && item.status === \"pending\") {\n return {\n ...workState,\n state: State.Processing,\n currentBatch: 1,\n };\n }\n\n if (workState.state === \"processing\") {\n if (item.status === \"done\") {\n return {\n ...workState,\n itemsProcessed: workState.itemsProcessed + 1,\n currentBatch: workState.currentBatch + 1,\n };\n }\n\n if (item.status === \"error\") {\n return {\n ...workState,\n itemsError: workState.itemsError + 1,\n state: State.Complete,\n };\n }\n }\n\n return workState;\n};\n\nconst stateStream = Stream.fromIterable([\n { id: 1, status: \"pending\" as const },\n { id: 2, status: \"done\" as const },\n { id: 3, status: \"done\" as const },\n { id: 4, status: \"error\" as const },\n]).pipe(\n Stream.scan(\n { state: State.Idle, itemsProcessed: 0, itemsError: 0, currentBatch: 0 },\n stateTransition\n ),\n Stream.tap((state) =>\n Effect.log(`State: ${state.state}, Processed: ${state.itemsProcessed}`)\n )\n);\n```\n\n---\n\n## Advanced: Windowed Aggregation\n\nTrack statistics over sliding windows:\n\n```typescript\ninterface WindowedStats {\n readonly windowStart: number;\n readonly windowEnd: number;\n readonly count: number;\n readonly sum: number;\n readonly average: number;\n}\n\nconst slidingWindowStats = (\n stream: Stream.Stream,\n windowSizeMs: number\n): Stream.Stream => {\n const initialWindow: WindowedStats = {\n windowStart: Date.now(),\n windowEnd: Date.now() + windowSizeMs,\n count: 0,\n sum: 0,\n average: 0,\n };\n\n return stream.pipe(\n Stream.scan(initialWindow, (window, item) => {\n const now = Date.now();\n\n // Check if outside current window\n if (now > window.windowEnd) {\n // Start new window\n return {\n windowStart: now,\n windowEnd: now + windowSizeMs,\n count: 1,\n sum: item.value,\n average: item.value,\n };\n }\n\n // Add to current window\n const newCount = window.count + 1;\n const newSum = window.sum + item.value;\n\n return {\n ...window,\n count: newCount,\n sum: newSum,\n average: newSum / newCount,\n };\n })\n );\n};\n\n// Usage: Track 5-second windowed average\nconst windowedMetrics = slidingWindowStats(\n Stream.fromIterable(\n Array.from({ length: 100 }, (_, i) => ({ value: Math.random() * 100 }))\n ),\n 5000\n).pipe(\n Stream.tap((window) =>\n Effect.log(\n `Window avg: ${window.average.toFixed(2)}, count: ${window.count}`\n )\n )\n);\n```\n\n---\n\n## Advanced: Conditional Accumulation\n\nApply different accumulation rules based on values:\n\n```typescript\ninterface ConditionalStats {\n readonly allCount: number;\n readonly evenSum: number;\n readonly oddSum: number;\n readonly largeCount: number;\n}\n\nconst conditionalFold = (\n stream: Stream.Stream\n): Stream.Stream =>\n stream.pipe(\n Stream.fold(\n {\n allCount: 0,\n evenSum: 0,\n oddSum: 0,\n largeCount: 0,\n },\n (stats, value) => ({\n allCount: stats.allCount + 1,\n evenSum: value % 2 === 0 ? stats.evenSum + value : stats.evenSum,\n oddSum: value % 2 !== 0 ? stats.oddSum + value : stats.oddSum,\n largeCount: value > 50 ? stats.largeCount + 1 : stats.largeCount,\n })\n )\n );\n\n// Usage\nconst conditionalStats = conditionalFold(\n Stream.fromIterable([10, 25, 40, 55, 70, 85])\n).pipe(\n Stream.tap((stats) =>\n Effect.log(\n `Even sum: ${stats.evenSum}, Odd sum: ${stats.oddSum}, Large: ${stats.largeCount}`\n )\n )\n);\n```\n\n---\n\n## Advanced: Error Accumulation\n\nCollect errors while processing stream:\n\n```typescript\ninterface ProcessResult {\n readonly succeeded: number;\n readonly failed: number;\n readonly errors: Error[];\n}\n\nconst processWithErrorCollection = (\n stream: Stream.Stream,\n process: (item: A) => Effect.Effect\n): Effect.Effect =>\n stream.pipe(\n Stream.fold(\n { succeeded: 0, failed: 0, errors: [] },\n async (result, item) => {\n try {\n await process(item);\n return { ...result, succeeded: result.succeeded + 1 };\n } catch (error) {\n return {\n ...result,\n failed: result.failed + 1,\n errors: [...result.errors, error as Error],\n };\n }\n }\n ),\n Stream.take(1),\n Stream.runCollect,\n Effect.map((chunk) => Chunk.head(chunk)?._tag === \"Some\" ? Chunk.head(chunk)!.value : { succeeded: 0, failed: 0, errors: [] })\n );\n\n// Usage: Process items, collect errors\nconst errorCollection = processWithErrorCollection(\n Stream.fromIterable([1, 2, 3, 4, 5]),\n (item) =>\n Effect.gen(function* () {\n if (item === 3) {\n throw new Error(\"Processing failed\");\n }\n yield* Effect.log(`Processed: ${item}`);\n })\n);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use scan when:**\n\n- Tracking state across elements\n- Need intermediate values (running total, count)\n- State transitions based on elements\n- Debugging stream values at stages\n\n✅ **Use fold when:**\n\n- Need only final aggregate\n- Computing single result from stream\n- Collecting summary statistics\n- Building final data structure\n\n⚠️ **Trade-offs:**\n\n- Accumulator type becomes state carrier\n- Complex accumulator types hard to debug\n- Errors in reducer crash stream\n- State not shared outside stream\n\n---\n\n## Scan vs Fold vs Reduce\n\n| Operator | Emits | Use Case |\n| --- | --- | --- |\n| **scan** | Intermediate + final | Running statistics, state trace |\n| **fold** | Only final | Summary aggregate |\n| **reduce** | Only final | Non-empty stream aggregation |\n\n---\n\n## See Also\n\n- [Stream Pattern 1: Map & Filter Transformations](./stream-pattern-map-filter-transformations.mdx) - Stream transformations\n- [Stream Collect Results](./stream-collect-results.mdx) - Collecting stream output\n- [Manage Shared State with Ref](./manage-shared-state-with-ref.mdx) - External state management\n- [Process Streaming Data with Stream](./process-streaming-data-with-stream.mdx) - Stream basics" - }, - { - "id": "stream-pattern-grouping-windowing", - "title": "Stream Pattern 5: Grouping and Windowing Streams", - "description": "Use groupBy to partition streams by key and tumbling/sliding windows to aggregate streams over time windows.", - "skillLevel": "advanced", - "useCases": [ - "streams" - ], - "example": "This example demonstrates windowing and grouping patterns.\n\n```typescript\nimport { Effect, Stream, Ref, Duration, Schedule } from \"effect\";\n\ninterface Event {\n readonly timestamp: Date;\n readonly userId: string;\n readonly action: string;\n readonly duration: number; // milliseconds\n}\n\n// Simulate event stream\nconst generateEvents = (): Event[] => [\n { timestamp: new Date(Date.now() - 5000), userId: \"user1\", action: \"click\", duration: 100 },\n { timestamp: new Date(Date.now() - 4500), userId: \"user2\", action: \"view\", duration: 250 },\n { timestamp: new Date(Date.now() - 4000), userId: \"user1\", action: \"scroll\", duration: 150 },\n { timestamp: new Date(Date.now() - 3500), userId: \"user3\", action: \"click\", duration: 120 },\n { timestamp: new Date(Date.now() - 3000), userId: \"user2\", action: \"click\", duration: 180 },\n { timestamp: new Date(Date.now() - 2500), userId: \"user1\", action: \"view\", duration: 200 },\n { timestamp: new Date(Date.now() - 2000), userId: \"user3\", action: \"view\", duration: 300 },\n { timestamp: new Date(Date.now() - 1500), userId: \"user1\", action: \"submit\", duration: 500 },\n { timestamp: new Date(Date.now() - 1000), userId: \"user2\", action: \"scroll\", duration: 100 },\n];\n\n// Main: windowing and grouping examples\nconst program = Effect.gen(function* () {\n console.log(`\\n[WINDOWING & GROUPING] Stream organization patterns\\n`);\n\n const events = generateEvents();\n\n // Example 1: Tumbling window (fixed-size batches)\n console.log(`[1] Tumbling window (2-event batches):\\n`);\n\n const windowSize = 2;\n let batchNumber = 1;\n\n for (let i = 0; i < events.length; i += windowSize) {\n const batch = events.slice(i, i + windowSize);\n\n yield* Effect.log(`[WINDOW ${batchNumber}] (${batch.length} events)`);\n\n let totalDuration = 0;\n\n for (const event of batch) {\n yield* Effect.log(\n ` - ${event.userId}: ${event.action} (${event.duration}ms)`\n );\n\n totalDuration += event.duration;\n }\n\n yield* Effect.log(`[WINDOW ${batchNumber}] Total duration: ${totalDuration}ms\\n`);\n\n batchNumber++;\n }\n\n // Example 2: Sliding window (overlapping)\n console.log(`[2] Sliding window (last 3 events, slide by 1):\\n`);\n\n const windowSizeSlide = 3;\n const slideBy = 1;\n\n for (let i = 0; i <= events.length - windowSizeSlide; i += slideBy) {\n const window = events.slice(i, i + windowSizeSlide);\n\n const avgDuration =\n window.reduce((sum, e) => sum + e.duration, 0) / window.length;\n\n yield* Effect.log(\n `[SLIDE ${i / slideBy}] ${window.length} events, avg duration: ${avgDuration.toFixed(0)}ms`\n );\n }\n\n // Example 3: Group by key\n console.log(`\\n[3] Group by user:\\n`);\n\n const byUser = new Map();\n\n for (const event of events) {\n if (!byUser.has(event.userId)) {\n byUser.set(event.userId, []);\n }\n\n byUser.get(event.userId)!.push(event);\n }\n\n for (const [userId, userEvents] of byUser) {\n const totalActions = userEvents.length;\n const totalTime = userEvents.reduce((sum, e) => sum + e.duration, 0);\n const avgTime = totalTime / totalActions;\n\n yield* Effect.log(\n `[USER ${userId}] ${totalActions} actions, ${totalTime}ms total, ${avgTime.toFixed(0)}ms avg`\n );\n }\n\n // Example 4: Group + Window combination\n console.log(`\\n[4] Group by user, window by action type:\\n`);\n\n for (const [userId, userEvents] of byUser) {\n const byAction = new Map();\n\n for (const event of userEvents) {\n if (!byAction.has(event.action)) {\n byAction.set(event.action, []);\n }\n\n byAction.get(event.action)!.push(event);\n }\n\n yield* Effect.log(`[USER ${userId}] Action breakdown:`);\n\n for (const [action, actionEvents] of byAction) {\n const count = actionEvents.length;\n const total = actionEvents.reduce((sum, e) => sum + e.duration, 0);\n\n yield* Effect.log(` ${action}: ${count}x (${total}ms total)`);\n }\n }\n\n // Example 5: Session window (based on inactivity timeout)\n console.log(`\\n[5] Session window (gap > 1000ms = new session):\\n`);\n\n const sessionGapMs = 1000;\n const sessions: Event[][] = [];\n let currentSession: Event[] = [];\n let lastTimestamp = events[0]?.timestamp.getTime() ?? 0;\n\n for (const event of events) {\n const currentTime = event.timestamp.getTime();\n const timeSinceLastEvent = currentTime - lastTimestamp;\n\n if (timeSinceLastEvent > sessionGapMs && currentSession.length > 0) {\n sessions.push(currentSession);\n yield* Effect.log(\n `[SESSION] Closed (${currentSession.length} events, gap: ${timeSinceLastEvent}ms)`\n );\n\n currentSession = [];\n }\n\n currentSession.push(event);\n lastTimestamp = currentTime;\n }\n\n if (currentSession.length > 0) {\n sessions.push(currentSession);\n yield* Effect.log(`[SESSION] Final (${currentSession.length} events)`);\n }\n\n // Example 6: Top-K aggregation in window\n console.log(`\\n[6] Top 2 actions in last window:\\n`);\n\n const lastWindow = events.slice(-3);\n\n const actionCounts = new Map();\n\n for (const event of lastWindow) {\n actionCounts.set(\n event.action,\n (actionCounts.get(event.action) ?? 0) + 1\n );\n }\n\n const topActions = Array.from(actionCounts.entries())\n .sort((a, b) => b[1] - a[1])\n .slice(0, 2);\n\n yield* Effect.log(`[TOP-K] In last window of 3 events:`);\n\n for (const [action, count] of topActions) {\n yield* Effect.log(` ${action}: ${count}x`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Unbounded streams need boundaries:\n\n**Problem 1: Memory exhaustion**\n- Processing 1M events with no boundary = keep all in memory\n- Cumulative memory grows unbounded\n- Eventually OOM error\n\n**Problem 2: Late aggregation**\n- Can't sum all events until stream ends (never)\n- Need to decide: \"sum events in this 1-second window\"\n\n**Problem 3: Grouping complexity**\n- Stream of user events: need per-user aggregation\n- Without groupBy: manual state tracking (error-prone)\n\n**Problem 4: Temporal patterns**\n- \"Top 10 searches in last 5 minutes\" requires windowing\n- \"Average response time per endpoint per minute\" requires grouping + windowing\n\nSolutions:\n\n**Tumbling window**:\n- Divide stream into 1-sec, 5-sec, or 1-min chunks\n- Process each chunk independently\n- Clear memory between windows\n- Natural for: metrics, batching, reports\n\n**Sliding window**:\n- Keep last 5 minutes of data at all times\n- Emit updated aggregation every second\n- Detect patterns over overlapping periods\n- Natural for: anomaly detection, trends\n\n**Group by**:\n- Separate streams by key\n- Each key has independent state\n- Emit grouped results\n- Natural for: per-user, per-endpoint, per-tenant\n\n---", - "content": "## Guideline\n\nWindowing organizes unbounded streams into bounded chunks:\n\n- **Tumbling window**: Fixed-size non-overlapping (e.g., 1-sec windows)\n- **Sliding window**: Overlapping windows (e.g., 10-sec window, 5-sec hop)\n- **Group by key**: Partition stream by field value\n- **Session window**: Event-based windows (e.g., idle timeout)\n- **Batch aggregation**: Process N items or wait T seconds\n\nPattern: `Stream.groupBy()`, custom windowing with `Ref` and `Schedule`\n\n---\n\n## Rationale\n\nUnbounded streams need boundaries:\n\n**Problem 1: Memory exhaustion**\n- Processing 1M events with no boundary = keep all in memory\n- Cumulative memory grows unbounded\n- Eventually OOM error\n\n**Problem 2: Late aggregation**\n- Can't sum all events until stream ends (never)\n- Need to decide: \"sum events in this 1-second window\"\n\n**Problem 3: Grouping complexity**\n- Stream of user events: need per-user aggregation\n- Without groupBy: manual state tracking (error-prone)\n\n**Problem 4: Temporal patterns**\n- \"Top 10 searches in last 5 minutes\" requires windowing\n- \"Average response time per endpoint per minute\" requires grouping + windowing\n\nSolutions:\n\n**Tumbling window**:\n- Divide stream into 1-sec, 5-sec, or 1-min chunks\n- Process each chunk independently\n- Clear memory between windows\n- Natural for: metrics, batching, reports\n\n**Sliding window**:\n- Keep last 5 minutes of data at all times\n- Emit updated aggregation every second\n- Detect patterns over overlapping periods\n- Natural for: anomaly detection, trends\n\n**Group by**:\n- Separate streams by key\n- Each key has independent state\n- Emit grouped results\n- Natural for: per-user, per-endpoint, per-tenant\n\n---\n\n## Good Example\n\nThis example demonstrates windowing and grouping patterns.\n\n```typescript\nimport { Effect, Stream, Ref, Duration, Schedule } from \"effect\";\n\ninterface Event {\n readonly timestamp: Date;\n readonly userId: string;\n readonly action: string;\n readonly duration: number; // milliseconds\n}\n\n// Simulate event stream\nconst generateEvents = (): Event[] => [\n { timestamp: new Date(Date.now() - 5000), userId: \"user1\", action: \"click\", duration: 100 },\n { timestamp: new Date(Date.now() - 4500), userId: \"user2\", action: \"view\", duration: 250 },\n { timestamp: new Date(Date.now() - 4000), userId: \"user1\", action: \"scroll\", duration: 150 },\n { timestamp: new Date(Date.now() - 3500), userId: \"user3\", action: \"click\", duration: 120 },\n { timestamp: new Date(Date.now() - 3000), userId: \"user2\", action: \"click\", duration: 180 },\n { timestamp: new Date(Date.now() - 2500), userId: \"user1\", action: \"view\", duration: 200 },\n { timestamp: new Date(Date.now() - 2000), userId: \"user3\", action: \"view\", duration: 300 },\n { timestamp: new Date(Date.now() - 1500), userId: \"user1\", action: \"submit\", duration: 500 },\n { timestamp: new Date(Date.now() - 1000), userId: \"user2\", action: \"scroll\", duration: 100 },\n];\n\n// Main: windowing and grouping examples\nconst program = Effect.gen(function* () {\n console.log(`\\n[WINDOWING & GROUPING] Stream organization patterns\\n`);\n\n const events = generateEvents();\n\n // Example 1: Tumbling window (fixed-size batches)\n console.log(`[1] Tumbling window (2-event batches):\\n`);\n\n const windowSize = 2;\n let batchNumber = 1;\n\n for (let i = 0; i < events.length; i += windowSize) {\n const batch = events.slice(i, i + windowSize);\n\n yield* Effect.log(`[WINDOW ${batchNumber}] (${batch.length} events)`);\n\n let totalDuration = 0;\n\n for (const event of batch) {\n yield* Effect.log(\n ` - ${event.userId}: ${event.action} (${event.duration}ms)`\n );\n\n totalDuration += event.duration;\n }\n\n yield* Effect.log(`[WINDOW ${batchNumber}] Total duration: ${totalDuration}ms\\n`);\n\n batchNumber++;\n }\n\n // Example 2: Sliding window (overlapping)\n console.log(`[2] Sliding window (last 3 events, slide by 1):\\n`);\n\n const windowSizeSlide = 3;\n const slideBy = 1;\n\n for (let i = 0; i <= events.length - windowSizeSlide; i += slideBy) {\n const window = events.slice(i, i + windowSizeSlide);\n\n const avgDuration =\n window.reduce((sum, e) => sum + e.duration, 0) / window.length;\n\n yield* Effect.log(\n `[SLIDE ${i / slideBy}] ${window.length} events, avg duration: ${avgDuration.toFixed(0)}ms`\n );\n }\n\n // Example 3: Group by key\n console.log(`\\n[3] Group by user:\\n`);\n\n const byUser = new Map();\n\n for (const event of events) {\n if (!byUser.has(event.userId)) {\n byUser.set(event.userId, []);\n }\n\n byUser.get(event.userId)!.push(event);\n }\n\n for (const [userId, userEvents] of byUser) {\n const totalActions = userEvents.length;\n const totalTime = userEvents.reduce((sum, e) => sum + e.duration, 0);\n const avgTime = totalTime / totalActions;\n\n yield* Effect.log(\n `[USER ${userId}] ${totalActions} actions, ${totalTime}ms total, ${avgTime.toFixed(0)}ms avg`\n );\n }\n\n // Example 4: Group + Window combination\n console.log(`\\n[4] Group by user, window by action type:\\n`);\n\n for (const [userId, userEvents] of byUser) {\n const byAction = new Map();\n\n for (const event of userEvents) {\n if (!byAction.has(event.action)) {\n byAction.set(event.action, []);\n }\n\n byAction.get(event.action)!.push(event);\n }\n\n yield* Effect.log(`[USER ${userId}] Action breakdown:`);\n\n for (const [action, actionEvents] of byAction) {\n const count = actionEvents.length;\n const total = actionEvents.reduce((sum, e) => sum + e.duration, 0);\n\n yield* Effect.log(` ${action}: ${count}x (${total}ms total)`);\n }\n }\n\n // Example 5: Session window (based on inactivity timeout)\n console.log(`\\n[5] Session window (gap > 1000ms = new session):\\n`);\n\n const sessionGapMs = 1000;\n const sessions: Event[][] = [];\n let currentSession: Event[] = [];\n let lastTimestamp = events[0]?.timestamp.getTime() ?? 0;\n\n for (const event of events) {\n const currentTime = event.timestamp.getTime();\n const timeSinceLastEvent = currentTime - lastTimestamp;\n\n if (timeSinceLastEvent > sessionGapMs && currentSession.length > 0) {\n sessions.push(currentSession);\n yield* Effect.log(\n `[SESSION] Closed (${currentSession.length} events, gap: ${timeSinceLastEvent}ms)`\n );\n\n currentSession = [];\n }\n\n currentSession.push(event);\n lastTimestamp = currentTime;\n }\n\n if (currentSession.length > 0) {\n sessions.push(currentSession);\n yield* Effect.log(`[SESSION] Final (${currentSession.length} events)`);\n }\n\n // Example 6: Top-K aggregation in window\n console.log(`\\n[6] Top 2 actions in last window:\\n`);\n\n const lastWindow = events.slice(-3);\n\n const actionCounts = new Map();\n\n for (const event of lastWindow) {\n actionCounts.set(\n event.action,\n (actionCounts.get(event.action) ?? 0) + 1\n );\n }\n\n const topActions = Array.from(actionCounts.entries())\n .sort((a, b) => b[1] - a[1])\n .slice(0, 2);\n\n yield* Effect.log(`[TOP-K] In last window of 3 events:`);\n\n for (const [action, count] of topActions) {\n yield* Effect.log(` ${action}: ${count}x`);\n }\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Custom Windowing with State\n\nBuild sophisticated windows using Ref:\n\n```typescript\ninterface Window {\n id: string;\n items: A[];\n openTime: Date;\n closeTime: Date;\n}\n\nconst createWindowManager = (config: {\n windowDurationMs: number;\n maxItemsPerWindow: number;\n onWindowClose: (window: Window) => Effect.Effect;\n}) =>\n Effect.gen(function* () {\n const currentWindow = yield* Ref.make>({\n id: `window-${Date.now()}`,\n items: [],\n openTime: new Date(),\n closeTime: new Date(Date.now() + config.windowDurationMs),\n });\n\n const addItem = (item: A) =>\n Effect.gen(function* () {\n const window = yield* Ref.get(currentWindow);\n\n if (\n window.items.length >= config.maxItemsPerWindow ||\n Date.now() >= window.closeTime.getTime()\n ) {\n // Close current window and open new one\n yield* config.onWindowClose(window);\n\n yield* Ref.set(currentWindow, {\n id: `window-${Date.now()}`,\n items: [item],\n openTime: new Date(),\n closeTime: new Date(Date.now() + config.windowDurationMs),\n });\n } else {\n // Add to current window\n yield* Ref.modify(currentWindow, (w) => [\n undefined,\n { ...w, items: [...w.items, item] },\n ]);\n }\n });\n\n return { addItem, currentWindow };\n });\n\n// Usage\nconst windowManager = createWindowManager({\n windowDurationMs: 1000,\n maxItemsPerWindow: 10,\n onWindowClose: (window) =>\n Effect.log(`[WINDOW CLOSED] ${window.id}: ${window.items.length} items`),\n});\n```\n\n---\n\n## Advanced: Stateful Grouping with Cleanup\n\nManage per-key state with automatic cleanup:\n\n```typescript\ninterface GroupState {\n value: V;\n lastUpdated: Date;\n accessCount: number;\n}\n\nconst createGroupedAggregator = (config: {\n initialValue: V;\n update: (current: V, newItem: unknown) => V;\n ttlMs: number;\n maxGroups: number;\n onEvict: (key: K, value: V) => Effect.Effect;\n}) =>\n Effect.gen(function* () {\n const groups = yield* Ref.make>>(new Map());\n\n const updateGroup = (key: K, item: unknown) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(groups);\n\n if (!current.has(key)) {\n // New group\n if (current.size >= config.maxGroups) {\n // Evict oldest group\n const [oldestKey, oldest] = Array.from(current.entries()).reduce(\n (a, b) =>\n a[1].lastUpdated < b[1].lastUpdated ? a : b\n );\n\n yield* config.onEvict(oldestKey, oldest.value);\n\n const updated = new Map(current);\n updated.delete(oldestKey);\n yield* Ref.set(groups, updated);\n }\n\n // Add new group\n yield* Ref.modify(groups, (g) => [\n undefined,\n new Map(g).set(key, {\n value: config.initialValue,\n lastUpdated: new Date(),\n accessCount: 1,\n }),\n ]);\n } else {\n // Update existing group\n const group = current.get(key)!;\n\n yield* Ref.modify(groups, (g) => [\n undefined,\n new Map(g).set(key, {\n value: config.update(group.value, item),\n lastUpdated: new Date(),\n accessCount: group.accessCount + 1,\n }),\n ]);\n }\n });\n\n const cleanup = Effect.gen(function* () {\n const now = Date.now();\n const current = yield* Ref.get(groups);\n\n const expired = Array.from(current.entries()).filter(\n ([_, state]) => now - state.lastUpdated.getTime() > config.ttlMs\n );\n\n for (const [key, state] of expired) {\n yield* config.onEvict(key, state.value);\n current.delete(key);\n }\n });\n\n return { updateGroup, cleanup, groups };\n });\n```\n\n---\n\n## Advanced: Time-Based Window Integration\n\nIntegrate with scheduling for time windows:\n\n```typescript\nconst createTimeWindowedStream = (\n stream: Stream.Stream,\n config: {\n windowSize: Duration.Duration;\n compute: (items: A[]) => Effect.Effect;\n }\n) =>\n Effect.gen(function* () {\n const buffer = yield* Ref.make([]);\n\n const windowTick = Effect.gen(function* () {\n const items = yield* Ref.get(buffer);\n\n if (items.length > 0) {\n const result = yield* config.compute(items);\n\n yield* Effect.log(`[WINDOW] Computed result: ${JSON.stringify(result)}`);\n\n yield* Ref.set(buffer, []);\n }\n }).pipe(\n Effect.repeat(\n Schedule.fixed(config.windowSize)\n )\n );\n\n const consume = stream.pipe(\n Stream.tap((item) =>\n Ref.modify(buffer, (items) => [undefined, [...items, item]])\n )\n );\n\n return [consume, windowTick] as const;\n });\n```\n\n---\n\n## Performance Considerations\n\n| Pattern | Memory | CPU | Latency | Use Case |\n| --- | --- | --- | --- | --- |\n| **Tumbling** | Low | Low | Mid | Batching, reports |\n| **Sliding** | High | Medium | Low | Real-time metrics |\n| **Group by** | Medium | Medium | Low | Per-entity aggregation |\n| **Session** | Medium | Medium | High | User session tracking |\n| **Top-K** | Low | High | Medium | Leaderboards |\n\n---\n\n## When to Use This Pattern\n\n✅ **Use tumbling windows when:**\n- Batching for efficiency\n- End-of-period reports\n- Memory matters\n- Natural period (per hour, day)\n\n✅ **Use sliding windows when:**\n- Real-time monitoring\n- Detect trends\n- Latency-sensitive\n- Memory available\n\n✅ **Use grouping when:**\n- Per-entity aggregation\n- Multi-tenant isolation\n- User analytics\n- Sharded processing\n\n⚠️ **Trade-offs:**\n- Sliding windows use more memory\n- Group state management complexity\n- Cleanup/eviction policies matter\n- Tuning window sizes critical\n\n---\n\n## Window Size Tuning\n\n| Metric | Too Small | Too Large |\n| --- | --- | --- |\n| **Latency** | Constant churn | Long delays |\n| **Accuracy** | Noisy results | Stale aggregations |\n| **Memory** | More windows | Larger windows |\n| **CPU** | More computations | Fewer computations |\n\n**Recommendation**: Start with window size = desired reporting interval\n\n---\n\n## See Also\n\n- [Stream Pattern 4: Stateful Operations](./stream-pattern-stateful-operations.mdx) - Fold/scan basis\n- [Stream Pattern 3: Backpressure Control](./stream-pattern-backpressure-control.mdx) - Buffering strategies\n- [Scheduling Pattern 1: Repeat on Interval](./scheduling-pattern-repeat-interval.mdx) - Time-based triggers\n- [Concurrency Pattern 3: Coordinate with Latch](./concurrency-pattern-coordinate-with-latch.mdx) - Multi-event coordination" - }, - { - "id": "stream-pattern-resource-management", - "title": "Stream Pattern 6: Resource Management in Streams", - "description": "Use Stream.bracket or effect scoping to guarantee resource cleanup, preventing leaks even when streams fail or are interrupted.", - "skillLevel": "advanced", - "useCases": [ - "streams" - ], - "example": "This example demonstrates resource acquisition, use, and guaranteed cleanup.\n\n```typescript\nimport { Effect, Stream, Resource, Scope, Ref } from \"effect\";\n\ninterface FileHandle {\n readonly path: string;\n readonly fd: number;\n}\n\ninterface Connection {\n readonly id: string;\n readonly isOpen: boolean;\n}\n\n// Simulate resource management\nconst program = Effect.gen(function* () {\n console.log(`\\n[RESOURCE MANAGEMENT] Stream resource lifecycle\\n`);\n\n // Example 1: Bracket pattern for file streams\n console.log(`[1] Bracket pattern (acquire → use → release):\\n`);\n\n let openHandles = 0;\n let closedHandles = 0;\n\n const openFile = (path: string) =>\n Effect.gen(function* () {\n openHandles++;\n yield* Effect.log(`[OPEN] File \"${path}\" (total open: ${openHandles})`);\n\n return { path, fd: 1000 + openHandles };\n });\n\n const closeFile = (handle: FileHandle) =>\n Effect.gen(function* () {\n closedHandles++;\n yield* Effect.log(`[CLOSE] File \"${handle.path}\" (total closed: ${closedHandles})`);\n });\n\n const readFileWithBracket = (path: string) =>\n Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(path);\n\n yield* Effect.log(\n `[USE] Reading from fd ${handle.fd} (\"${handle.path}\")`\n );\n\n // Simulate reading\n return \"file contents\";\n } finally {\n // Guaranteed to run even if error occurs above\n if (handle) {\n yield* closeFile(handle);\n }\n }\n });\n\n // Test with success\n yield* Effect.log(`[TEST] Success case:`);\n\n const content = yield* readFileWithBracket(\"/data/file.txt\");\n\n yield* Effect.log(`[RESULT] Got: \"${content}\"\\n`);\n\n // Test with failure (simulated)\n yield* Effect.log(`[TEST] Error case:`);\n\n const failCase = Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(\"/data/missing.txt\");\n\n // Simulate error mid-operation\n yield* Effect.fail(new Error(\"Read failed\"));\n } finally {\n if (handle) {\n yield* closeFile(handle);\n }\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Caught: ${error.message}`);\n yield* Effect.log(`[CHECK] Closed handles: ${closedHandles} (verifying cleanup)\\n`);\n })\n )\n );\n\n yield* failCase;\n\n // Example 2: Connection pool management\n console.log(`[2] Connection pooling:\\n`);\n\n interface ConnectionPool {\n acquire: () => Effect.Effect;\n release: (conn: Connection) => Effect.Effect;\n }\n\n const createConnectionPool = (maxSize: number): Effect.Effect =>\n Effect.gen(function* () {\n const available = yield* Ref.make([]);\n const inUse = yield* Ref.make>(new Set());\n let idCounter = 0;\n\n return {\n acquire: Effect.gen(function* () {\n const avail = yield* Ref.get(available);\n\n if (avail.length > 0) {\n yield* Effect.log(`[POOL] Reusing connection from pool`);\n\n const conn = avail.pop()!;\n\n yield* Ref.modify(inUse, (set) => [\n undefined,\n new Set(set).add(conn.id),\n ]);\n\n return conn;\n }\n\n const inUseCount = (yield* Ref.get(inUse)).size;\n\n if (inUseCount >= maxSize) {\n yield* Effect.fail(new Error(\"Pool exhausted\"));\n }\n\n const connId = `conn-${++idCounter}`;\n\n yield* Effect.log(`[POOL] Creating new connection: ${connId}`);\n\n const conn = { id: connId, isOpen: true };\n\n yield* Ref.modify(inUse, (set) => [\n undefined,\n new Set(set).add(connId),\n ]);\n\n return conn;\n }),\n\n release: (conn: Connection) =>\n Effect.gen(function* () {\n yield* Ref.modify(inUse, (set) => {\n const updated = new Set(set);\n updated.delete(conn.id);\n return [undefined, updated];\n });\n\n yield* Ref.modify(available, (avail) => [\n undefined,\n [...avail, conn],\n ]);\n\n yield* Effect.log(`[POOL] Returned connection: ${conn.id}`);\n }),\n };\n });\n\n const pool = yield* createConnectionPool(3);\n\n // Acquire and release connections\n const conn1 = yield* pool.acquire();\n const conn2 = yield* pool.acquire();\n\n yield* pool.release(conn1);\n\n const conn3 = yield* pool.acquire(); // Reuses conn1\n\n yield* Effect.log(`\\n`);\n\n // Example 3: Scope-based resource safety\n console.log(`[3] Scoped resources (hierarchical cleanup):\\n`);\n\n let scopedCount = 0;\n\n const withScoped = (create: () => Effect.Effect) =>\n Effect.gen(function* () {\n scopedCount++;\n const id = scopedCount;\n\n yield* Effect.log(`[SCOPE] Enter scope ${id}`);\n\n const resource = yield* create();\n\n yield* Effect.log(`[SCOPE] Using resource in scope ${id}`);\n\n yield* Effect.sync(() => {\n // Cleanup happens here when scope exits\n yield* Effect.log(`[SCOPE] Exit scope ${id}`);\n }).pipe(\n Effect.ensuring(\n Effect.log(`[SCOPE] Cleanup guaranteed for scope ${id}`)\n )\n );\n\n return resource;\n });\n\n // Nested scopes\n const result = yield* withScoped(() =>\n Effect.succeed({\n level: 1,\n data: yield* withScoped(() => Effect.succeed(\"inner data\")),\n })\n ).pipe(\n Effect.catchAll(() => Effect.succeed({ level: 0, data: null }))\n );\n\n yield* Effect.log(`[SCOPES] Cleanup order: inner → outer\\n`);\n\n // Example 4: Stream resource management\n console.log(`[4] Stream with resource cleanup:\\n`);\n\n let streamResourceCount = 0;\n\n // Simulate stream that acquires resources\n const streamWithResources = Stream.empty.pipe(\n Stream.tap(() =>\n Effect.gen(function* () {\n streamResourceCount++;\n yield* Effect.log(`[STREAM-RES] Acquired resource ${streamResourceCount}`);\n })\n ),\n // Cleanup when stream ends\n Stream.ensuring(\n Effect.log(`[STREAM-RES] Cleaning up all ${streamResourceCount} resources`)\n )\n );\n\n yield* Stream.runDrain(streamWithResources);\n\n // Example 5: Error propagation with cleanup\n console.log(`\\n[5] Error safety with cleanup:\\n`);\n\n const safeRead = (retryCount: number) =>\n Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(`/data/file-${retryCount}.txt`);\n\n if (retryCount < 2) {\n yield* Effect.log(`[READ] Attempt ${retryCount}: failing intentionally`);\n yield* Effect.fail(new Error(`Attempt ${retryCount} failed`));\n }\n\n yield* Effect.log(`[READ] Success on attempt ${retryCount}`);\n\n return \"success\";\n } finally {\n if (handle) {\n yield* closeFile(handle);\n }\n }\n });\n\n // Retry with guaranteed cleanup\n const result2 = yield* safeRead(1).pipe(\n Effect.retry(\n Schedule.recurs(2).pipe(\n Schedule.compose(Schedule.fixed(\"10 millis\"))\n )\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[FINAL] All retries failed: ${error.message}`);\n return \"fallback\";\n })\n )\n );\n\n yield* Effect.log(`\\n[FINAL] Result: ${result2}`);\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Streams without resource management cause problems:\n\n**Problem 1: Resource exhaustion**\n- Open file streams without closing → file descriptor limit exceeded\n- Get connections from pool, never return → connection starvation\n- System becomes unresponsive\n\n**Problem 2: Memory leaks**\n- Stream emits large objects → memory grows\n- Without cleanup → garbage persists\n- GC can't reclaim\n\n**Problem 3: Data corruption**\n- Write to file without flush → partial writes on crash\n- Read from connection while another thread writes → data race\n- Results are unpredictable\n\n**Problem 4: Silent failures**\n- Resource cleanup fails → error lost\n- Application proceeds as if successful\n- Hidden bug becomes hard-to-trace crash later\n\nSolutions:\n\n**Bracket pattern**:\n- Acquire resource\n- Use resource (even if error)\n- Always release resource\n- Track errors separately\n\n**Resource scopes**:\n- Nested resource management\n- Parent cleanup waits for children\n- Hierarchical resource graphs\n- Type-safe guarantees\n\n**Connection pooling**:\n- Reuse connections\n- Track available/in-use\n- Prevent exhaustion\n- Support graceful shutdown\n\n---", - "content": "## Guideline\n\nStreams must clean up resources deterministically:\n\n- **Acquire/Release**: Get resource, use, return resource\n- **Bracket pattern**: Ensure cleanup on success or failure\n- **Scope safety**: Guarantee cleanup even on exceptions\n- **Connection pooling**: Reuse connections, prevent exhaustion\n- **Concurrent cleanup**: Handle cleanup under concurrency\n\nPattern: `Stream.bracket()`, `Resource.make()`, `Scope` for resource safety\n\n---\n\n## Rationale\n\nStreams without resource management cause problems:\n\n**Problem 1: Resource exhaustion**\n- Open file streams without closing → file descriptor limit exceeded\n- Get connections from pool, never return → connection starvation\n- System becomes unresponsive\n\n**Problem 2: Memory leaks**\n- Stream emits large objects → memory grows\n- Without cleanup → garbage persists\n- GC can't reclaim\n\n**Problem 3: Data corruption**\n- Write to file without flush → partial writes on crash\n- Read from connection while another thread writes → data race\n- Results are unpredictable\n\n**Problem 4: Silent failures**\n- Resource cleanup fails → error lost\n- Application proceeds as if successful\n- Hidden bug becomes hard-to-trace crash later\n\nSolutions:\n\n**Bracket pattern**:\n- Acquire resource\n- Use resource (even if error)\n- Always release resource\n- Track errors separately\n\n**Resource scopes**:\n- Nested resource management\n- Parent cleanup waits for children\n- Hierarchical resource graphs\n- Type-safe guarantees\n\n**Connection pooling**:\n- Reuse connections\n- Track available/in-use\n- Prevent exhaustion\n- Support graceful shutdown\n\n---\n\n## Good Example\n\nThis example demonstrates resource acquisition, use, and guaranteed cleanup.\n\n```typescript\nimport { Effect, Stream, Resource, Scope, Ref } from \"effect\";\n\ninterface FileHandle {\n readonly path: string;\n readonly fd: number;\n}\n\ninterface Connection {\n readonly id: string;\n readonly isOpen: boolean;\n}\n\n// Simulate resource management\nconst program = Effect.gen(function* () {\n console.log(`\\n[RESOURCE MANAGEMENT] Stream resource lifecycle\\n`);\n\n // Example 1: Bracket pattern for file streams\n console.log(`[1] Bracket pattern (acquire → use → release):\\n`);\n\n let openHandles = 0;\n let closedHandles = 0;\n\n const openFile = (path: string) =>\n Effect.gen(function* () {\n openHandles++;\n yield* Effect.log(`[OPEN] File \"${path}\" (total open: ${openHandles})`);\n\n return { path, fd: 1000 + openHandles };\n });\n\n const closeFile = (handle: FileHandle) =>\n Effect.gen(function* () {\n closedHandles++;\n yield* Effect.log(`[CLOSE] File \"${handle.path}\" (total closed: ${closedHandles})`);\n });\n\n const readFileWithBracket = (path: string) =>\n Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(path);\n\n yield* Effect.log(\n `[USE] Reading from fd ${handle.fd} (\"${handle.path}\")`\n );\n\n // Simulate reading\n return \"file contents\";\n } finally {\n // Guaranteed to run even if error occurs above\n if (handle) {\n yield* closeFile(handle);\n }\n }\n });\n\n // Test with success\n yield* Effect.log(`[TEST] Success case:`);\n\n const content = yield* readFileWithBracket(\"/data/file.txt\");\n\n yield* Effect.log(`[RESULT] Got: \"${content}\"\\n`);\n\n // Test with failure (simulated)\n yield* Effect.log(`[TEST] Error case:`);\n\n const failCase = Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(\"/data/missing.txt\");\n\n // Simulate error mid-operation\n yield* Effect.fail(new Error(\"Read failed\"));\n } finally {\n if (handle) {\n yield* closeFile(handle);\n }\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Caught: ${error.message}`);\n yield* Effect.log(`[CHECK] Closed handles: ${closedHandles} (verifying cleanup)\\n`);\n })\n )\n );\n\n yield* failCase;\n\n // Example 2: Connection pool management\n console.log(`[2] Connection pooling:\\n`);\n\n interface ConnectionPool {\n acquire: () => Effect.Effect;\n release: (conn: Connection) => Effect.Effect;\n }\n\n const createConnectionPool = (maxSize: number): Effect.Effect =>\n Effect.gen(function* () {\n const available = yield* Ref.make([]);\n const inUse = yield* Ref.make>(new Set());\n let idCounter = 0;\n\n return {\n acquire: Effect.gen(function* () {\n const avail = yield* Ref.get(available);\n\n if (avail.length > 0) {\n yield* Effect.log(`[POOL] Reusing connection from pool`);\n\n const conn = avail.pop()!;\n\n yield* Ref.modify(inUse, (set) => [\n undefined,\n new Set(set).add(conn.id),\n ]);\n\n return conn;\n }\n\n const inUseCount = (yield* Ref.get(inUse)).size;\n\n if (inUseCount >= maxSize) {\n yield* Effect.fail(new Error(\"Pool exhausted\"));\n }\n\n const connId = `conn-${++idCounter}`;\n\n yield* Effect.log(`[POOL] Creating new connection: ${connId}`);\n\n const conn = { id: connId, isOpen: true };\n\n yield* Ref.modify(inUse, (set) => [\n undefined,\n new Set(set).add(connId),\n ]);\n\n return conn;\n }),\n\n release: (conn: Connection) =>\n Effect.gen(function* () {\n yield* Ref.modify(inUse, (set) => {\n const updated = new Set(set);\n updated.delete(conn.id);\n return [undefined, updated];\n });\n\n yield* Ref.modify(available, (avail) => [\n undefined,\n [...avail, conn],\n ]);\n\n yield* Effect.log(`[POOL] Returned connection: ${conn.id}`);\n }),\n };\n });\n\n const pool = yield* createConnectionPool(3);\n\n // Acquire and release connections\n const conn1 = yield* pool.acquire();\n const conn2 = yield* pool.acquire();\n\n yield* pool.release(conn1);\n\n const conn3 = yield* pool.acquire(); // Reuses conn1\n\n yield* Effect.log(`\\n`);\n\n // Example 3: Scope-based resource safety\n console.log(`[3] Scoped resources (hierarchical cleanup):\\n`);\n\n let scopedCount = 0;\n\n const withScoped = (create: () => Effect.Effect) =>\n Effect.gen(function* () {\n scopedCount++;\n const id = scopedCount;\n\n yield* Effect.log(`[SCOPE] Enter scope ${id}`);\n\n const resource = yield* create();\n\n yield* Effect.log(`[SCOPE] Using resource in scope ${id}`);\n\n yield* Effect.sync(() => {\n // Cleanup happens here when scope exits\n yield* Effect.log(`[SCOPE] Exit scope ${id}`);\n }).pipe(\n Effect.ensuring(\n Effect.log(`[SCOPE] Cleanup guaranteed for scope ${id}`)\n )\n );\n\n return resource;\n });\n\n // Nested scopes\n const result = yield* withScoped(() =>\n Effect.succeed({\n level: 1,\n data: yield* withScoped(() => Effect.succeed(\"inner data\")),\n })\n ).pipe(\n Effect.catchAll(() => Effect.succeed({ level: 0, data: null }))\n );\n\n yield* Effect.log(`[SCOPES] Cleanup order: inner → outer\\n`);\n\n // Example 4: Stream resource management\n console.log(`[4] Stream with resource cleanup:\\n`);\n\n let streamResourceCount = 0;\n\n // Simulate stream that acquires resources\n const streamWithResources = Stream.empty.pipe(\n Stream.tap(() =>\n Effect.gen(function* () {\n streamResourceCount++;\n yield* Effect.log(`[STREAM-RES] Acquired resource ${streamResourceCount}`);\n })\n ),\n // Cleanup when stream ends\n Stream.ensuring(\n Effect.log(`[STREAM-RES] Cleaning up all ${streamResourceCount} resources`)\n )\n );\n\n yield* Stream.runDrain(streamWithResources);\n\n // Example 5: Error propagation with cleanup\n console.log(`\\n[5] Error safety with cleanup:\\n`);\n\n const safeRead = (retryCount: number) =>\n Effect.gen(function* () {\n let handle: FileHandle | null = null;\n\n try {\n handle = yield* openFile(`/data/file-${retryCount}.txt`);\n\n if (retryCount < 2) {\n yield* Effect.log(`[READ] Attempt ${retryCount}: failing intentionally`);\n yield* Effect.fail(new Error(`Attempt ${retryCount} failed`));\n }\n\n yield* Effect.log(`[READ] Success on attempt ${retryCount}`);\n\n return \"success\";\n } finally {\n if (handle) {\n yield* closeFile(handle);\n }\n }\n });\n\n // Retry with guaranteed cleanup\n const result2 = yield* safeRead(1).pipe(\n Effect.retry(\n Schedule.recurs(2).pipe(\n Schedule.compose(Schedule.fixed(\"10 millis\"))\n )\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[FINAL] All retries failed: ${error.message}`);\n return \"fallback\";\n })\n )\n );\n\n yield* Effect.log(`\\n[FINAL] Result: ${result2}`);\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Resource Acquisition with Effect.acquire\n\nBuild safe resource APIs:\n\n```typescript\n// Safe resource pattern using Effect.acquire\nconst withDatabaseConnection = (\n operation: (conn: Connection) => Effect.Effect\n): Effect.Effect =>\n Effect.gen(function* () {\n const conn = yield* Effect.acquire(\n Effect.gen(function* () {\n const connection = { id: \"db-1\", isOpen: true };\n\n yield* Effect.log(`[DB] Opened connection`);\n\n // Return release effect\n return Effect.gen(function* () {\n yield* Effect.log(`[DB] Closed connection`);\n });\n })\n );\n\n return yield* operation(conn);\n });\n\n// Usage - cleanup guaranteed\nconst dbOperation = withDatabaseConnection((conn) =>\n Effect.gen(function* () {\n yield* Effect.log(`[DB] Using connection: ${conn.id}`);\n return \"query result\";\n })\n);\n```\n\n---\n\n## Advanced: Concurrent Resource Cleanup\n\nHandle cleanup under concurrency:\n\n```typescript\nconst createConcurrentResourcePool = (config: {\n createResource: () => Effect.Effect;\n destroyResource: (r: R) => Effect.Effect;\n maxConcurrent: number;\n}) =>\n Effect.gen(function* () {\n const resources = yield* Ref.make([]);\n const inUse = yield* Ref.make>(new Set());\n\n const withResource = (\n use: (resource: R) => Effect.Effect\n ): Effect.Effect =>\n Effect.gen(function* () {\n let resource: R;\n\n // Acquire\n const existing = yield* Ref.get(resources);\n\n if (existing.length > 0) {\n resource = existing.pop()!;\n } else {\n resource = yield* config.createResource();\n }\n\n const resourceId = Math.random();\n\n yield* Ref.modify(inUse, (set) =>\n [undefined, new Set(set).add(resourceId)]\n );\n\n try {\n return yield* use(resource);\n } finally {\n // Release - guaranteed\n yield* Ref.modify(inUse, (set) => {\n const updated = new Set(set);\n updated.delete(resourceId);\n return [undefined, updated];\n });\n\n yield* Ref.modify(resources, (list) => [\n undefined,\n [...list, resource],\n ]);\n }\n });\n\n const shutdownAll = Effect.gen(function* () {\n const all = yield* Ref.get(resources);\n\n for (const resource of all) {\n yield* config.destroyResource(resource);\n }\n\n const stillInUse = yield* Ref.get(inUse);\n\n if (stillInUse.size > 0) {\n yield* Effect.log(\n `[WARNING] ${stillInUse.size} resources still in use during shutdown`\n );\n }\n });\n\n return { withResource, shutdownAll };\n });\n```\n\n---\n\n## Advanced: Graceful Stream Shutdown\n\nEnsure all resources clean up on interruption:\n\n```typescript\nconst createGracefulStream = (\n source: Stream.Stream,\n config: {\n shutdown: () => Effect.Effect;\n timeout: Duration.Duration;\n }\n) =>\n source.pipe(\n Stream.ensuring(\n Effect.gen(function* () {\n yield* Effect.log(`[STREAM] Starting graceful shutdown`);\n\n yield* config.shutdown.pipe(\n Effect.timeout(config.timeout),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(\n `[STREAM] Shutdown timeout or failed: ${error.message}`\n );\n })\n )\n );\n\n yield* Effect.log(`[STREAM] Shutdown complete`);\n })\n )\n );\n```\n\n---\n\n## Common Resource Patterns\n\n| Pattern | Use Case | Cleanup |\n| --- | --- | --- |\n| **Bracket** | Simple acquire/release | Finally block |\n| **Resource.make** | Complex setup | Built-in cleanup |\n| **Scope** | Hierarchical resources | Nested cleanup |\n| **Pool** | Reuse connections | Return to pool |\n| **Stream.bracket** | Stream elements | Per-element cleanup |\n\n---\n\n## When to Use This Pattern\n\n✅ **Use brackets when:**\n- File I/O operations\n- Database connections\n- Network sockets\n- Temporary resources\n\n✅ **Use pools when:**\n- Expensive resource creation\n- Connection reuse\n- Resource scarcity\n- Performance-critical\n\n✅ **Use scopes when:**\n- Hierarchical resources\n- Complex resource graphs\n- Nested allocation\n- Type-safe guarantees\n\n⚠️ **Trade-offs:**\n- Complexity increases\n- More error cases to handle\n- Debugging harder\n- Performance impact\n\n---\n\n## Resource Cleanup Checklist\n\n- ✅ Always use try/finally or bracket\n- ✅ Test error paths explicitly\n- ✅ Verify cleanup on timeout\n- ✅ Test concurrent access\n- ✅ Monitor for resource leaks\n- ✅ Set appropriate timeouts\n- ✅ Log resource lifecycle\n- ✅ Plan graceful shutdown\n\n---\n\n## See Also\n\n- [Platform Pattern 2: FileSystem Operations](./platform-filesystem-operations.mdx) - File resource patterns\n- [Platform Pattern 3: Key-Value Storage](./platform-keyvaluestore-persistence.mdx) - Persistence resources\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error safety\n- [Concurrency Pattern 3: Latch Coordination](./concurrency-pattern-coordinate-with-latch.mdx) - Synchronized cleanup" - }, - { - "id": "stream-pattern-error-handling", - "title": "Stream Pattern 7: Error Handling in Streams", - "description": "Use Stream error handlers to recover from failures, retry operations, and maintain stream integrity even when individual elements fail.", - "skillLevel": "advanced", - "useCases": [ - "streams" - ], - "example": "This example demonstrates stream error handling patterns.\n\n```typescript\nimport { Effect, Stream, Ref } from \"effect\";\n\ninterface DataRecord {\n id: string;\n value: number;\n}\n\ninterface ProcessingResult {\n successful: DataRecord[];\n failed: Array<{ id: string; error: string }>;\n retried: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[STREAM ERROR HANDLING] Resilient stream processing\\n`);\n\n // Example 1: Continue on error (skip failed, process rest)\n console.log(`[1] Continue processing despite errors:\\n`);\n\n const processElement = (record: DataRecord): Effect.Effect =>\n Effect.gen(function* () {\n if (record.value < 0) {\n yield* Effect.fail(new Error(`Invalid value: ${record.value}`));\n }\n\n return `processed-${record.id}`;\n });\n\n const records = [\n { id: \"rec1\", value: 10 },\n { id: \"rec2\", value: -5 }, // Will fail\n { id: \"rec3\", value: 20 },\n { id: \"rec4\", value: -1 }, // Will fail\n { id: \"rec5\", value: 30 },\n ];\n\n const successfulProcessing = yield* Stream.fromIterable(records).pipe(\n Stream.mapEffect((record) =>\n processElement(record).pipe(\n Effect.map((result) => ({ success: true, result })),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Record ${record.id} failed`);\n\n return { success: false, error };\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[RESULTS] ${successfulProcessing.filter((r) => r.success).length}/${records.length} succeeded\\n`\n );\n\n // Example 2: Recover with fallback value\n console.log(`[2] Providing fallback on error:\\n`);\n\n const getData = (id: string): Effect.Effect =>\n id.includes(\"fail\") ? Effect.fail(new Error(\"Data error\")) : Effect.succeed(42);\n\n const recovered = yield* Stream.fromIterable([\"ok1\", \"fail1\", \"ok2\"]).pipe(\n Stream.mapEffect((id) =>\n getData(id).pipe(\n Effect.catchAll(() =>\n Effect.gen(function* () {\n yield* Effect.log(`[FALLBACK] Using default for ${id}`);\n\n return -1; // Fallback value\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(`[VALUES] ${recovered.join(\", \")}\\n`);\n\n // Example 3: Collect errors alongside successes\n console.log(`[3] Collecting errors and successes:\\n`);\n\n const results = yield* Ref.make({\n successful: [],\n failed: [],\n retried: 0,\n });\n\n yield* Stream.fromIterable(records).pipe(\n Stream.mapEffect((record) =>\n processElement(record).pipe(\n Effect.tap((result) =>\n Ref.modify(results, (r) => [\n undefined,\n {\n ...r,\n successful: [...r.successful, record],\n },\n ])\n ),\n Effect.catchAll((error) =>\n Ref.modify(results, (r) => [\n undefined,\n {\n ...r,\n failed: [\n ...r.failed,\n { id: record.id, error: error.message },\n ],\n },\n ])\n )\n )\n ),\n Stream.runDrain\n );\n\n const finalResults = yield* Ref.get(results);\n\n yield* Effect.log(\n `[AGGREGATE] ${finalResults.successful.length} succeeded, ${finalResults.failed.length} failed`\n );\n\n for (const failure of finalResults.failed) {\n yield* Effect.log(` - ${failure.id}: ${failure.error}`);\n }\n\n // Example 4: Retry on error with backoff\n console.log(`\\n[4] Retry with exponential backoff:\\n`);\n\n let attemptCount = 0;\n\n const unreliableOperation = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n attemptCount++;\n\n if (attemptCount <= 2) {\n yield* Effect.log(`[ATTEMPT ${attemptCount}] Failing for ${id}`);\n\n yield* Effect.fail(new Error(\"Temporary failure\"));\n }\n\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attemptCount}`);\n\n return `result-${id}`;\n });\n\n const retried = unreliableOperation(\"test\").pipe(\n Effect.retry(\n Schedule.exponential(\"10 millis\").pipe(\n Schedule.upTo(\"100 millis\"),\n Schedule.recurs(3)\n )\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[EXHAUSTED] All retries failed`);\n\n return \"fallback\";\n })\n )\n );\n\n yield* retried;\n\n // Example 5: Error context in streams\n console.log(`\\n[5] Propagating error context:\\n`);\n\n interface StreamContext {\n batchId: string;\n timestamp: Date;\n }\n\n const processWithContext = (context: StreamContext) =>\n Stream.fromIterable([1, 2, -3, 4]).pipe(\n Stream.mapEffect((value) =>\n Effect.gen(function* () {\n if (value < 0) {\n yield* Effect.fail(\n new Error(\n `Negative value in batch ${context.batchId} at ${context.timestamp.toISOString()}`\n )\n );\n }\n\n return value * 2;\n })\n ),\n Stream.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONTEXT ERROR] ${error.message}`);\n\n return Stream.empty;\n })\n )\n );\n\n const context: StreamContext = {\n batchId: \"batch-001\",\n timestamp: new Date(),\n };\n\n yield* processWithContext(context).pipe(Stream.runDrain);\n\n // Example 6: Partial recovery (keep good data, log bad)\n console.log(`\\n[6] Partial recovery strategy:\\n`);\n\n const mixedQuality = [\n { id: \"1\", data: \"good\" },\n { id: \"2\", data: \"bad\" },\n { id: \"3\", data: \"good\" },\n { id: \"4\", data: \"bad\" },\n { id: \"5\", data: \"good\" },\n ];\n\n const processQuality = (record: { id: string; data: string }) =>\n record.data === \"good\"\n ? Effect.succeed(`valid-${record.id}`)\n : Effect.fail(new Error(`Invalid data for ${record.id}`));\n\n const partialResults = yield* Stream.fromIterable(mixedQuality).pipe(\n Stream.mapEffect((record) =>\n processQuality(record).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[LOG] ${error.message}`);\n\n return null; // Skip this record\n })\n )\n )\n ),\n Stream.filter((result) => result !== null),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[PARTIAL] Kept ${partialResults.length}/${mixedQuality.length} valid records\\n`\n );\n\n // Example 7: Timeout handling in streams\n console.log(`[7] Timeout handling per element:\\n`);\n\n const slowOperation = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate slow operations\n if (id === \"slow\") {\n yield* Effect.sleep(\"200 millis\");\n } else {\n yield* Effect.sleep(\"50 millis\");\n }\n\n return `done-${id}`;\n });\n\n const withTimeout = yield* Stream.fromIterable([\"fast1\", \"slow\", \"fast2\"]).pipe(\n Stream.mapEffect((id) =>\n slowOperation(id).pipe(\n Effect.timeout(\"100 millis\"),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[TIMEOUT] Operation ${id} timed out`);\n\n return \"timeout-fallback\";\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(`[RESULTS] ${withTimeout.join(\", \")}\\n`);\n\n // Example 8: Stream termination on critical error\n console.log(`[8] Terminating stream on critical error:\\n`);\n\n const isCritical = (error: Error): boolean =>\n error.message.includes(\"CRITICAL\");\n\n const terminateOnCritical = Stream.fromIterable([1, 2, 3]).pipe(\n Stream.mapEffect((value) =>\n value === 2\n ? Effect.fail(new Error(\"CRITICAL: System failure\"))\n : Effect.succeed(value)\n ),\n Stream.catchAll((error) =>\n Effect.gen(function* () {\n if (isCritical(error)) {\n yield* Effect.log(`[CRITICAL] Terminating stream`);\n\n return Stream.fail(error);\n }\n\n yield* Effect.log(`[WARNING] Continuing despite error`);\n\n return Stream.empty;\n })\n )\n );\n\n yield* terminateOnCritical.pipe(\n Stream.runCollect,\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STOPPED] Stream stopped: ${error.message}`);\n\n return [];\n })\n )\n );\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Errors in streams cause cascading failures:\n\n**Problem 1: Stream death**\n- Process 10,000 records\n- Record #5000 has bad data\n- Stream crashes\n- 9,000 records not processed\n- Manual re-run needed\n\n**Problem 2: Silent data loss**\n- Stream encounters error\n- Stops processing\n- Caller doesn't notice\n- Missing data goes undetected\n- Reports wrong numbers\n\n**Problem 3: No recovery visibility**\n- Error happens\n- Is it retried? How many times?\n- Did it recover?\n- Silent guessing required\n\n**Problem 4: Downstream effects**\n- Stream error affects all subscribers\n- Cascading failure\n- System becomes unavailable\n- All downstream blocked\n\nSolutions:\n\n**Continue on error**:\n- Skip failed element\n- Process rest of stream\n- Collect error for later\n- Partial success acceptable\n\n**Retry with backoff**:\n- Transient error? Retry\n- Exponential backoff\n- Eventually give up\n- Move to next element\n\n**Error aggregation**:\n- Collect all errors\n- Collect all successes\n- Report both\n- Analytics/debugging\n\n**Graceful termination**:\n- Signal end of stream on error\n- Allow cleanup\n- Prevent resource leak\n- Controlled shutdown\n\n---", - "content": "## Guideline\n\nStream error handling enables resilience:\n\n- **Continue on error**: Skip failed element, process rest\n- **Recover**: Provide fallback value\n- **Retry**: Attempt failed element again\n- **Aggregate**: Collect errors alongside successful values\n- **Terminate gracefully**: Controlled shutdown\n- **Propagate**: Let errors flow upstream\n\nPattern: `Stream.catchAll()`, `Stream.retry()`, `Stream.recover()`, `Stream.runCollect()`\n\n---\n\n## Rationale\n\nErrors in streams cause cascading failures:\n\n**Problem 1: Stream death**\n- Process 10,000 records\n- Record #5000 has bad data\n- Stream crashes\n- 9,000 records not processed\n- Manual re-run needed\n\n**Problem 2: Silent data loss**\n- Stream encounters error\n- Stops processing\n- Caller doesn't notice\n- Missing data goes undetected\n- Reports wrong numbers\n\n**Problem 3: No recovery visibility**\n- Error happens\n- Is it retried? How many times?\n- Did it recover?\n- Silent guessing required\n\n**Problem 4: Downstream effects**\n- Stream error affects all subscribers\n- Cascading failure\n- System becomes unavailable\n- All downstream blocked\n\nSolutions:\n\n**Continue on error**:\n- Skip failed element\n- Process rest of stream\n- Collect error for later\n- Partial success acceptable\n\n**Retry with backoff**:\n- Transient error? Retry\n- Exponential backoff\n- Eventually give up\n- Move to next element\n\n**Error aggregation**:\n- Collect all errors\n- Collect all successes\n- Report both\n- Analytics/debugging\n\n**Graceful termination**:\n- Signal end of stream on error\n- Allow cleanup\n- Prevent resource leak\n- Controlled shutdown\n\n---\n\n## Good Example\n\nThis example demonstrates stream error handling patterns.\n\n```typescript\nimport { Effect, Stream, Ref } from \"effect\";\n\ninterface DataRecord {\n id: string;\n value: number;\n}\n\ninterface ProcessingResult {\n successful: DataRecord[];\n failed: Array<{ id: string; error: string }>;\n retried: number;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[STREAM ERROR HANDLING] Resilient stream processing\\n`);\n\n // Example 1: Continue on error (skip failed, process rest)\n console.log(`[1] Continue processing despite errors:\\n`);\n\n const processElement = (record: DataRecord): Effect.Effect =>\n Effect.gen(function* () {\n if (record.value < 0) {\n yield* Effect.fail(new Error(`Invalid value: ${record.value}`));\n }\n\n return `processed-${record.id}`;\n });\n\n const records = [\n { id: \"rec1\", value: 10 },\n { id: \"rec2\", value: -5 }, // Will fail\n { id: \"rec3\", value: 20 },\n { id: \"rec4\", value: -1 }, // Will fail\n { id: \"rec5\", value: 30 },\n ];\n\n const successfulProcessing = yield* Stream.fromIterable(records).pipe(\n Stream.mapEffect((record) =>\n processElement(record).pipe(\n Effect.map((result) => ({ success: true, result })),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[ERROR] Record ${record.id} failed`);\n\n return { success: false, error };\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[RESULTS] ${successfulProcessing.filter((r) => r.success).length}/${records.length} succeeded\\n`\n );\n\n // Example 2: Recover with fallback value\n console.log(`[2] Providing fallback on error:\\n`);\n\n const getData = (id: string): Effect.Effect =>\n id.includes(\"fail\") ? Effect.fail(new Error(\"Data error\")) : Effect.succeed(42);\n\n const recovered = yield* Stream.fromIterable([\"ok1\", \"fail1\", \"ok2\"]).pipe(\n Stream.mapEffect((id) =>\n getData(id).pipe(\n Effect.catchAll(() =>\n Effect.gen(function* () {\n yield* Effect.log(`[FALLBACK] Using default for ${id}`);\n\n return -1; // Fallback value\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(`[VALUES] ${recovered.join(\", \")}\\n`);\n\n // Example 3: Collect errors alongside successes\n console.log(`[3] Collecting errors and successes:\\n`);\n\n const results = yield* Ref.make({\n successful: [],\n failed: [],\n retried: 0,\n });\n\n yield* Stream.fromIterable(records).pipe(\n Stream.mapEffect((record) =>\n processElement(record).pipe(\n Effect.tap((result) =>\n Ref.modify(results, (r) => [\n undefined,\n {\n ...r,\n successful: [...r.successful, record],\n },\n ])\n ),\n Effect.catchAll((error) =>\n Ref.modify(results, (r) => [\n undefined,\n {\n ...r,\n failed: [\n ...r.failed,\n { id: record.id, error: error.message },\n ],\n },\n ])\n )\n )\n ),\n Stream.runDrain\n );\n\n const finalResults = yield* Ref.get(results);\n\n yield* Effect.log(\n `[AGGREGATE] ${finalResults.successful.length} succeeded, ${finalResults.failed.length} failed`\n );\n\n for (const failure of finalResults.failed) {\n yield* Effect.log(` - ${failure.id}: ${failure.error}`);\n }\n\n // Example 4: Retry on error with backoff\n console.log(`\\n[4] Retry with exponential backoff:\\n`);\n\n let attemptCount = 0;\n\n const unreliableOperation = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n attemptCount++;\n\n if (attemptCount <= 2) {\n yield* Effect.log(`[ATTEMPT ${attemptCount}] Failing for ${id}`);\n\n yield* Effect.fail(new Error(\"Temporary failure\"));\n }\n\n yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attemptCount}`);\n\n return `result-${id}`;\n });\n\n const retried = unreliableOperation(\"test\").pipe(\n Effect.retry(\n Schedule.exponential(\"10 millis\").pipe(\n Schedule.upTo(\"100 millis\"),\n Schedule.recurs(3)\n )\n ),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[EXHAUSTED] All retries failed`);\n\n return \"fallback\";\n })\n )\n );\n\n yield* retried;\n\n // Example 5: Error context in streams\n console.log(`\\n[5] Propagating error context:\\n`);\n\n interface StreamContext {\n batchId: string;\n timestamp: Date;\n }\n\n const processWithContext = (context: StreamContext) =>\n Stream.fromIterable([1, 2, -3, 4]).pipe(\n Stream.mapEffect((value) =>\n Effect.gen(function* () {\n if (value < 0) {\n yield* Effect.fail(\n new Error(\n `Negative value in batch ${context.batchId} at ${context.timestamp.toISOString()}`\n )\n );\n }\n\n return value * 2;\n })\n ),\n Stream.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[CONTEXT ERROR] ${error.message}`);\n\n return Stream.empty;\n })\n )\n );\n\n const context: StreamContext = {\n batchId: \"batch-001\",\n timestamp: new Date(),\n };\n\n yield* processWithContext(context).pipe(Stream.runDrain);\n\n // Example 6: Partial recovery (keep good data, log bad)\n console.log(`\\n[6] Partial recovery strategy:\\n`);\n\n const mixedQuality = [\n { id: \"1\", data: \"good\" },\n { id: \"2\", data: \"bad\" },\n { id: \"3\", data: \"good\" },\n { id: \"4\", data: \"bad\" },\n { id: \"5\", data: \"good\" },\n ];\n\n const processQuality = (record: { id: string; data: string }) =>\n record.data === \"good\"\n ? Effect.succeed(`valid-${record.id}`)\n : Effect.fail(new Error(`Invalid data for ${record.id}`));\n\n const partialResults = yield* Stream.fromIterable(mixedQuality).pipe(\n Stream.mapEffect((record) =>\n processQuality(record).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[LOG] ${error.message}`);\n\n return null; // Skip this record\n })\n )\n )\n ),\n Stream.filter((result) => result !== null),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[PARTIAL] Kept ${partialResults.length}/${mixedQuality.length} valid records\\n`\n );\n\n // Example 7: Timeout handling in streams\n console.log(`[7] Timeout handling per element:\\n`);\n\n const slowOperation = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate slow operations\n if (id === \"slow\") {\n yield* Effect.sleep(\"200 millis\");\n } else {\n yield* Effect.sleep(\"50 millis\");\n }\n\n return `done-${id}`;\n });\n\n const withTimeout = yield* Stream.fromIterable([\"fast1\", \"slow\", \"fast2\"]).pipe(\n Stream.mapEffect((id) =>\n slowOperation(id).pipe(\n Effect.timeout(\"100 millis\"),\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[TIMEOUT] Operation ${id} timed out`);\n\n return \"timeout-fallback\";\n })\n )\n )\n ),\n Stream.runCollect\n );\n\n yield* Effect.log(`[RESULTS] ${withTimeout.join(\", \")}\\n`);\n\n // Example 8: Stream termination on critical error\n console.log(`[8] Terminating stream on critical error:\\n`);\n\n const isCritical = (error: Error): boolean =>\n error.message.includes(\"CRITICAL\");\n\n const terminateOnCritical = Stream.fromIterable([1, 2, 3]).pipe(\n Stream.mapEffect((value) =>\n value === 2\n ? Effect.fail(new Error(\"CRITICAL: System failure\"))\n : Effect.succeed(value)\n ),\n Stream.catchAll((error) =>\n Effect.gen(function* () {\n if (isCritical(error)) {\n yield* Effect.log(`[CRITICAL] Terminating stream`);\n\n return Stream.fail(error);\n }\n\n yield* Effect.log(`[WARNING] Continuing despite error`);\n\n return Stream.empty;\n })\n )\n );\n\n yield* terminateOnCritical.pipe(\n Stream.runCollect,\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[STOPPED] Stream stopped: ${error.message}`);\n\n return [];\n })\n )\n );\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Error Recovery Strategies\n\nBuild sophisticated recovery pipelines:\n\n```typescript\ninterface RecoveryStrategy {\n canRecover: (error: Error) => boolean;\n recover: (error: Error, element: unknown) =>\n Effect.Effect | null;\n priority: number;\n}\n\nconst applyRecoveryStrategies = (\n stream: Stream.Stream,\n strategies: RecoveryStrategy[]\n) =>\n stream.pipe(\n Stream.catchAll((error) =>\n Effect.gen(function* () {\n const sorted = strategies.sort((a, b) =>\n b.priority - a.priority\n );\n\n for (const strategy of sorted) {\n if (strategy.canRecover(error)) {\n const recovered = yield* strategy.recover(error, null).pipe(\n Effect.catchAll(() => Effect.fail(error))\n );\n\n return Stream.succeed(recovered);\n }\n }\n\n return Stream.fail(error);\n })\n )\n );\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use error recovery when:**\n- Stream processes many items\n- Some failures acceptable\n- Need partial success\n- Resilience matters\n- Want to continue processing\n\n✅ **Use retry when:**\n- Transient errors likely\n- Temporary failures common\n- Recovery expected\n- Backoff needed\n\n⚠️ **Trade-offs:**\n- More error cases to handle\n- Complexity increases\n- Debugging harder\n- Performance impact\n\n---\n\n## Error Handling Strategies\n\n| Strategy | When | Trade-off |\n| --- | --- | --- |\n| **Continue** | Some errors OK | Partial data |\n| **Retry** | Transient failures | Latency overhead |\n| **Aggregate** | Need all errors | More memory |\n| **Terminate** | Critical errors | Data loss |\n| **Fallback** | Recovery possible | Approximation |\n\n---\n\n## See Also\n\n- [Error Handling Pattern 2: Propagation](./error-handling-pattern-propagation.mdx) - Error chains\n- [Error Handling Pattern 3: Custom Strategies](./error-handling-pattern-custom-strategies.mdx) - Custom errors\n- [Scheduling Pattern 5: Advanced Retries](./scheduling-pattern-advanced-retry-chains.mdx) - Retry patterns\n- [Stream Pattern 6: Resource Management](./stream-pattern-resource-management.mdx) - Safe cleanup" - }, - { - "id": "stream-pattern-advanced-transformations", - "title": "Stream Pattern 8: Advanced Stream Transformations", - "description": "Use advanced stream operators to build sophisticated data pipelines that compose elegantly and maintain performance at scale.", - "skillLevel": "advanced", - "useCases": [ - "streams" - ], - "example": "This example demonstrates advanced stream transformations.\n\n```typescript\nimport { Effect, Stream, Ref, Chunk } from \"effect\";\n\ninterface LogEntry {\n timestamp: Date;\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n context?: Record;\n}\n\ninterface Metric {\n name: string;\n value: number;\n tags: Record;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED STREAM TRANSFORMATIONS] Complex data flows\\n`);\n\n // Example 1: Custom filter operator\n console.log(`[1] Custom filter with effect-based logic:\\n`);\n\n const filterByEffect = (\n predicate: (a: A) => Effect.Effect\n ) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.mapEffect((value) =>\n predicate(value).pipe(\n Effect.map((keep) => (keep ? value : null))\n )\n ),\n Stream.filter((value) => value !== null)\n );\n\n const isValid = (num: number): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate validation effect (e.g., API call)\n return num > 0 && num < 100;\n });\n\n const numbers = [50, 150, 25, -10, 75];\n\n const validNumbers = yield* Stream.fromIterable(numbers).pipe(\n filterByEffect(isValid),\n Stream.runCollect\n );\n\n yield* Effect.log(`[VALID] ${validNumbers.join(\", \")}\\n`);\n\n // Example 2: Enrichment transformation\n console.log(`[2] Enriching records with additional data:\\n`);\n\n interface RawRecord {\n id: string;\n value: number;\n }\n\n interface EnrichedRecord {\n id: string;\n value: number;\n validated: boolean;\n processed: Date;\n metadata: Record;\n }\n\n const enrich = (record: RawRecord): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate lookup/validation\n const validated = record.value > 0;\n\n return {\n id: record.id,\n value: record.value,\n validated,\n processed: new Date(),\n metadata: { source: \"stream\" },\n };\n });\n\n const rawData = [\n { id: \"r1\", value: 10 },\n { id: \"r2\", value: -5 },\n { id: \"r3\", value: 20 },\n ];\n\n const enriched = yield* Stream.fromIterable(rawData).pipe(\n Stream.mapEffect((record) => enrich(record)),\n Stream.runCollect\n );\n\n yield* Effect.log(`[ENRICHED] ${enriched.length} records enriched\\n`);\n\n // Example 3: Demultiplexing (split one stream into multiple)\n console.log(`[3] Demultiplexing by category:\\n`);\n\n interface Event {\n id: string;\n type: \"click\" | \"view\" | \"purchase\";\n data: unknown;\n }\n\n const events: Event[] = [\n { id: \"e1\", type: \"click\", data: { x: 100, y: 200 } },\n { id: \"e2\", type: \"view\", data: { url: \"/\" } },\n { id: \"e3\", type: \"purchase\", data: { amount: 99.99 } },\n { id: \"e4\", type: \"click\", data: { x: 50, y: 100 } },\n ];\n\n const clicks = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"click\"),\n Stream.runCollect\n );\n\n const views = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"view\"),\n Stream.runCollect\n );\n\n const purchases = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"purchase\"),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[DEMUX] Clicks: ${clicks.length}, Views: ${views.length}, Purchases: ${purchases.length}\\n`\n );\n\n // Example 4: Chunked processing (batch transformation)\n console.log(`[4] Chunked processing (batches of N):\\n`);\n\n const processChunk = (chunk: Array<{ id: string; value: number }>) =>\n Effect.gen(function* () {\n const sum = chunk.reduce((s, r) => s + r.value, 0);\n const avg = sum / chunk.length;\n\n yield* Effect.log(\n `[CHUNK] ${chunk.length} items, avg: ${avg.toFixed(2)}`\n );\n\n return { size: chunk.length, sum, avg };\n });\n\n const data = Array.from({ length: 10 }, (_, i) => ({\n id: `d${i}`,\n value: i + 1,\n }));\n\n const chunkSize = 3;\n const chunks = [];\n\n for (let i = 0; i < data.length; i += chunkSize) {\n const chunk = data.slice(i, i + chunkSize);\n\n chunks.push(chunk);\n }\n\n const chunkResults = yield* Effect.all(\n chunks.map((chunk) => processChunk(chunk))\n );\n\n yield* Effect.log(\n `[CHUNKS] Processed ${chunkResults.length} batches\\n`\n );\n\n // Example 5: Multi-stage transformation pipeline\n console.log(`[5] Multi-stage pipeline (parse → validate → transform):\\n`);\n\n const rawStrings = [\"10\", \"twenty\", \"30\", \"-5\", \"50\"];\n\n // Stage 1: Parse\n const parsed = yield* Stream.fromIterable(rawStrings).pipe(\n Stream.mapEffect((s) =>\n Effect.gen(function* () {\n try {\n return parseInt(s);\n } catch (error) {\n yield* Effect.fail(\n new Error(`Failed to parse: ${s}`)\n );\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[PARSE ERROR] ${error.message}`);\n\n return null;\n })\n )\n )\n ),\n Stream.filter((n) => n !== null),\n Stream.runCollect\n );\n\n yield* Effect.log(`[STAGE 1] Parsed: ${parsed.join(\", \")}`);\n\n // Stage 2: Validate\n const validated = parsed.filter((n) => n > 0);\n\n yield* Effect.log(`[STAGE 2] Validated: ${validated.join(\", \")}`);\n\n // Stage 3: Transform\n const transformed = validated.map((n) => n * 2);\n\n yield* Effect.log(`[STAGE 3] Transformed: ${transformed.join(\", \")}\\n`);\n\n // Example 6: Composition of custom operators\n console.log(`[6] Composable transformation pipeline:\\n`);\n\n // Define custom operator\n const withLogging = (label: string) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.tap((value) =>\n Effect.log(`[${label}] Processing: ${JSON.stringify(value)}`)\n )\n );\n\n const filterPositive = (stream: Stream.Stream) =>\n stream.pipe(\n Stream.filter((n) => n > 0),\n Stream.tap(() => Effect.log(`[FILTER] Kept positive`))\n );\n\n const scaleUp = (factor: number) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.map((n) => n * factor),\n Stream.tap((n) =>\n Effect.log(`[SCALE] Scaled to ${n}`)\n )\n );\n\n const testData = [10, -5, 20, -3, 30];\n\n const pipeline = yield* Stream.fromIterable(testData).pipe(\n withLogging(\"INPUT\"),\n filterPositive,\n scaleUp(10),\n Stream.runCollect\n );\n\n yield* Effect.log(`[RESULT] Final: ${pipeline.join(\", \")}\\n`);\n\n // Example 7: Stateful transformation\n console.log(`[7] Stateful transformation (running total):\\n`);\n\n const runningTotal = yield* Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.scan(0, (acc, value) => acc + value),\n Stream.runCollect\n );\n\n yield* Effect.log(`[TOTALS] ${runningTotal.join(\", \")}\\n`);\n\n // Example 8: Conditional transformation\n console.log(`[8] Conditional transformation (different paths):\\n`);\n\n interface Item {\n id: string;\n priority: \"high\" | \"normal\" | \"low\";\n }\n\n const transformByPriority = (item: Item): Effect.Effect<{\n id: string;\n processed: string;\n }> =>\n Effect.gen(function* () {\n switch (item.priority) {\n case \"high\":\n yield* Effect.log(`[HIGH] Priority processing for ${item.id}`);\n\n return { id: item.id, processed: \"urgent\" };\n\n case \"normal\":\n yield* Effect.log(\n `[NORMAL] Standard processing for ${item.id}`\n );\n\n return { id: item.id, processed: \"standard\" };\n\n case \"low\":\n yield* Effect.log(`[LOW] Deferred processing for ${item.id}`);\n\n return { id: item.id, processed: \"deferred\" };\n }\n });\n\n const items: Item[] = [\n { id: \"i1\", priority: \"normal\" },\n { id: \"i2\", priority: \"high\" },\n { id: \"i3\", priority: \"low\" },\n ];\n\n const processed = yield* Stream.fromIterable(items).pipe(\n Stream.mapEffect((item) => transformByPriority(item)),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[CONDITIONAL] Processed ${processed.length} items\\n`\n );\n\n // Example 9: Performance-optimized transformation\n console.log(`[9] Optimized for performance:\\n`);\n\n const largeDataset = Array.from({ length: 1000 }, (_, i) => i);\n\n const startTime = Date.now();\n\n // Use efficient operators\n const result = yield* Stream.fromIterable(largeDataset).pipe(\n Stream.filter((n) => n % 2 === 0), // Keep even\n Stream.take(100), // Limit to first 100\n Stream.map((n) => n * 2), // Transform\n Stream.runCollect\n );\n\n const elapsed = Date.now() - startTime;\n\n yield* Effect.log(\n `[PERF] Processed 1000 items in ${elapsed}ms, kept ${result.length} items`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "", - "explanation": "Simple transformations don't scale:\n\n**Problem 1: Performance degradation**\n- Each layer creates intermediate collection\n- 10 transformations = 10 allocations\n- Process 1M items = 10M allocations\n- GC pressure, memory exhaustion\n\n**Problem 2: Complex logic scattered**\n- Validation here, enrichment there, filtering elsewhere\n- Hard to maintain\n- Changes break other parts\n- No clear data flow\n\n**Problem 3: Effect handling**\n- Transformations need side effects\n- Network calls, database queries\n- Naive approach: load all, transform sequentially\n- Slow, inefficient\n\n**Problem 4: Reusability**\n- Custom transformation used once\n- Next time, rewrite from scratch\n- Code duplication\n- Bugs replicated\n\nSolutions:\n\n**Custom operators**:\n- Encapsulate transformation logic\n- Reusable across projects\n- Testable in isolation\n- Composable\n\n**Lazy evaluation**:\n- Compute as elements flow\n- No intermediate collections\n- Constant memory\n- Only compute what's used\n\n**Fusion**:\n- Combine multiple maps/filters\n- Single pass through data\n- No intermediate collections\n- Compiler/library optimizes\n\n**Effect composition**:\n- Chain effects naturally\n- Error propagation automatic\n- Resource cleanup guaranteed\n- Readable code\n\n---", - "content": "## Guideline\n\nAdvanced transformations enable complex data flows:\n\n- **Custom operators**: Build reusable transformations\n- **Effect-based**: Transformations with side effects\n- **Lazy evaluation**: Compute only what's needed\n- **Fusion**: Optimize composed operations\n- **Staging**: Multiple transformation layers\n- **Composition**: Combine operators cleanly\n\nPattern: `Stream.mapEffect()`, `Stream.map()`, pipe composition\n\n---\n\n## Rationale\n\nSimple transformations don't scale:\n\n**Problem 1: Performance degradation**\n- Each layer creates intermediate collection\n- 10 transformations = 10 allocations\n- Process 1M items = 10M allocations\n- GC pressure, memory exhaustion\n\n**Problem 2: Complex logic scattered**\n- Validation here, enrichment there, filtering elsewhere\n- Hard to maintain\n- Changes break other parts\n- No clear data flow\n\n**Problem 3: Effect handling**\n- Transformations need side effects\n- Network calls, database queries\n- Naive approach: load all, transform sequentially\n- Slow, inefficient\n\n**Problem 4: Reusability**\n- Custom transformation used once\n- Next time, rewrite from scratch\n- Code duplication\n- Bugs replicated\n\nSolutions:\n\n**Custom operators**:\n- Encapsulate transformation logic\n- Reusable across projects\n- Testable in isolation\n- Composable\n\n**Lazy evaluation**:\n- Compute as elements flow\n- No intermediate collections\n- Constant memory\n- Only compute what's used\n\n**Fusion**:\n- Combine multiple maps/filters\n- Single pass through data\n- No intermediate collections\n- Compiler/library optimizes\n\n**Effect composition**:\n- Chain effects naturally\n- Error propagation automatic\n- Resource cleanup guaranteed\n- Readable code\n\n---\n\n## Good Example\n\nThis example demonstrates advanced stream transformations.\n\n```typescript\nimport { Effect, Stream, Ref, Chunk } from \"effect\";\n\ninterface LogEntry {\n timestamp: Date;\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n context?: Record;\n}\n\ninterface Metric {\n name: string;\n value: number;\n tags: Record;\n}\n\nconst program = Effect.gen(function* () {\n console.log(`\\n[ADVANCED STREAM TRANSFORMATIONS] Complex data flows\\n`);\n\n // Example 1: Custom filter operator\n console.log(`[1] Custom filter with effect-based logic:\\n`);\n\n const filterByEffect = (\n predicate: (a: A) => Effect.Effect\n ) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.mapEffect((value) =>\n predicate(value).pipe(\n Effect.map((keep) => (keep ? value : null))\n )\n ),\n Stream.filter((value) => value !== null)\n );\n\n const isValid = (num: number): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate validation effect (e.g., API call)\n return num > 0 && num < 100;\n });\n\n const numbers = [50, 150, 25, -10, 75];\n\n const validNumbers = yield* Stream.fromIterable(numbers).pipe(\n filterByEffect(isValid),\n Stream.runCollect\n );\n\n yield* Effect.log(`[VALID] ${validNumbers.join(\", \")}\\n`);\n\n // Example 2: Enrichment transformation\n console.log(`[2] Enriching records with additional data:\\n`);\n\n interface RawRecord {\n id: string;\n value: number;\n }\n\n interface EnrichedRecord {\n id: string;\n value: number;\n validated: boolean;\n processed: Date;\n metadata: Record;\n }\n\n const enrich = (record: RawRecord): Effect.Effect =>\n Effect.gen(function* () {\n // Simulate lookup/validation\n const validated = record.value > 0;\n\n return {\n id: record.id,\n value: record.value,\n validated,\n processed: new Date(),\n metadata: { source: \"stream\" },\n };\n });\n\n const rawData = [\n { id: \"r1\", value: 10 },\n { id: \"r2\", value: -5 },\n { id: \"r3\", value: 20 },\n ];\n\n const enriched = yield* Stream.fromIterable(rawData).pipe(\n Stream.mapEffect((record) => enrich(record)),\n Stream.runCollect\n );\n\n yield* Effect.log(`[ENRICHED] ${enriched.length} records enriched\\n`);\n\n // Example 3: Demultiplexing (split one stream into multiple)\n console.log(`[3] Demultiplexing by category:\\n`);\n\n interface Event {\n id: string;\n type: \"click\" | \"view\" | \"purchase\";\n data: unknown;\n }\n\n const events: Event[] = [\n { id: \"e1\", type: \"click\", data: { x: 100, y: 200 } },\n { id: \"e2\", type: \"view\", data: { url: \"/\" } },\n { id: \"e3\", type: \"purchase\", data: { amount: 99.99 } },\n { id: \"e4\", type: \"click\", data: { x: 50, y: 100 } },\n ];\n\n const clicks = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"click\"),\n Stream.runCollect\n );\n\n const views = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"view\"),\n Stream.runCollect\n );\n\n const purchases = yield* Stream.fromIterable(events).pipe(\n Stream.filter((e) => e.type === \"purchase\"),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[DEMUX] Clicks: ${clicks.length}, Views: ${views.length}, Purchases: ${purchases.length}\\n`\n );\n\n // Example 4: Chunked processing (batch transformation)\n console.log(`[4] Chunked processing (batches of N):\\n`);\n\n const processChunk = (chunk: Array<{ id: string; value: number }>) =>\n Effect.gen(function* () {\n const sum = chunk.reduce((s, r) => s + r.value, 0);\n const avg = sum / chunk.length;\n\n yield* Effect.log(\n `[CHUNK] ${chunk.length} items, avg: ${avg.toFixed(2)}`\n );\n\n return { size: chunk.length, sum, avg };\n });\n\n const data = Array.from({ length: 10 }, (_, i) => ({\n id: `d${i}`,\n value: i + 1,\n }));\n\n const chunkSize = 3;\n const chunks = [];\n\n for (let i = 0; i < data.length; i += chunkSize) {\n const chunk = data.slice(i, i + chunkSize);\n\n chunks.push(chunk);\n }\n\n const chunkResults = yield* Effect.all(\n chunks.map((chunk) => processChunk(chunk))\n );\n\n yield* Effect.log(\n `[CHUNKS] Processed ${chunkResults.length} batches\\n`\n );\n\n // Example 5: Multi-stage transformation pipeline\n console.log(`[5] Multi-stage pipeline (parse → validate → transform):\\n`);\n\n const rawStrings = [\"10\", \"twenty\", \"30\", \"-5\", \"50\"];\n\n // Stage 1: Parse\n const parsed = yield* Stream.fromIterable(rawStrings).pipe(\n Stream.mapEffect((s) =>\n Effect.gen(function* () {\n try {\n return parseInt(s);\n } catch (error) {\n yield* Effect.fail(\n new Error(`Failed to parse: ${s}`)\n );\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.log(`[PARSE ERROR] ${error.message}`);\n\n return null;\n })\n )\n )\n ),\n Stream.filter((n) => n !== null),\n Stream.runCollect\n );\n\n yield* Effect.log(`[STAGE 1] Parsed: ${parsed.join(\", \")}`);\n\n // Stage 2: Validate\n const validated = parsed.filter((n) => n > 0);\n\n yield* Effect.log(`[STAGE 2] Validated: ${validated.join(\", \")}`);\n\n // Stage 3: Transform\n const transformed = validated.map((n) => n * 2);\n\n yield* Effect.log(`[STAGE 3] Transformed: ${transformed.join(\", \")}\\n`);\n\n // Example 6: Composition of custom operators\n console.log(`[6] Composable transformation pipeline:\\n`);\n\n // Define custom operator\n const withLogging = (label: string) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.tap((value) =>\n Effect.log(`[${label}] Processing: ${JSON.stringify(value)}`)\n )\n );\n\n const filterPositive = (stream: Stream.Stream) =>\n stream.pipe(\n Stream.filter((n) => n > 0),\n Stream.tap(() => Effect.log(`[FILTER] Kept positive`))\n );\n\n const scaleUp = (factor: number) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.map((n) => n * factor),\n Stream.tap((n) =>\n Effect.log(`[SCALE] Scaled to ${n}`)\n )\n );\n\n const testData = [10, -5, 20, -3, 30];\n\n const pipeline = yield* Stream.fromIterable(testData).pipe(\n withLogging(\"INPUT\"),\n filterPositive,\n scaleUp(10),\n Stream.runCollect\n );\n\n yield* Effect.log(`[RESULT] Final: ${pipeline.join(\", \")}\\n`);\n\n // Example 7: Stateful transformation\n console.log(`[7] Stateful transformation (running total):\\n`);\n\n const runningTotal = yield* Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.scan(0, (acc, value) => acc + value),\n Stream.runCollect\n );\n\n yield* Effect.log(`[TOTALS] ${runningTotal.join(\", \")}\\n`);\n\n // Example 8: Conditional transformation\n console.log(`[8] Conditional transformation (different paths):\\n`);\n\n interface Item {\n id: string;\n priority: \"high\" | \"normal\" | \"low\";\n }\n\n const transformByPriority = (item: Item): Effect.Effect<{\n id: string;\n processed: string;\n }> =>\n Effect.gen(function* () {\n switch (item.priority) {\n case \"high\":\n yield* Effect.log(`[HIGH] Priority processing for ${item.id}`);\n\n return { id: item.id, processed: \"urgent\" };\n\n case \"normal\":\n yield* Effect.log(\n `[NORMAL] Standard processing for ${item.id}`\n );\n\n return { id: item.id, processed: \"standard\" };\n\n case \"low\":\n yield* Effect.log(`[LOW] Deferred processing for ${item.id}`);\n\n return { id: item.id, processed: \"deferred\" };\n }\n });\n\n const items: Item[] = [\n { id: \"i1\", priority: \"normal\" },\n { id: \"i2\", priority: \"high\" },\n { id: \"i3\", priority: \"low\" },\n ];\n\n const processed = yield* Stream.fromIterable(items).pipe(\n Stream.mapEffect((item) => transformByPriority(item)),\n Stream.runCollect\n );\n\n yield* Effect.log(\n `[CONDITIONAL] Processed ${processed.length} items\\n`\n );\n\n // Example 9: Performance-optimized transformation\n console.log(`[9] Optimized for performance:\\n`);\n\n const largeDataset = Array.from({ length: 1000 }, (_, i) => i);\n\n const startTime = Date.now();\n\n // Use efficient operators\n const result = yield* Stream.fromIterable(largeDataset).pipe(\n Stream.filter((n) => n % 2 === 0), // Keep even\n Stream.take(100), // Limit to first 100\n Stream.map((n) => n * 2), // Transform\n Stream.runCollect\n );\n\n const elapsed = Date.now() - startTime;\n\n yield* Effect.log(\n `[PERF] Processed 1000 items in ${elapsed}ms, kept ${result.length} items`\n );\n});\n\nEffect.runPromise(program);\n```\n\n---\n\n## Advanced: Custom Stream Operator\n\nBuild reusable operators:\n\n```typescript\nconst throttleMap = (\n f: (a: A) => Effect.Effect,\n delayMs: number\n) =>\n (stream: Stream.Stream) =>\n stream.pipe(\n Stream.mapEffect((value) =>\n f(value).pipe(\n Effect.tap(() =>\n Effect.sleep(`${delayMs} millis`)\n )\n )\n )\n );\n\n// Usage\nconst throttled = stream.pipe(\n throttleMap((x) => Effect.succeed(x * 2), 10)\n);\n```\n\n---\n\n## Advanced: Composition Patterns\n\nCombine operators elegantly:\n\n```typescript\nconst compose = (\n ...operators: Array<(s: Stream.Stream) => Stream.Stream>\n) =>\n (stream: Stream.Stream) =>\n operators.reduce((s, op) => op(s), stream);\n\n// Usage\nconst pipeline = compose(\n filterPositive,\n scaleUp(10),\n withLogging(\"OUTPUT\")\n)(stream);\n```\n\n---\n\n## When to Use This Pattern\n\n✅ **Use advanced transformations when:**\n- Complex data flows\n- Performance critical\n- Reusable operators needed\n- Effect-based transformations\n- Multiple transformation layers\n\n✅ **Use composition when:**\n- Building pipelines\n- Reusing operators\n- Readable code needed\n- Testing individual steps\n\n⚠️ **Trade-offs:**\n- More abstraction\n- Debugging complexity\n- Learning curve\n- Potential overhead\n\n---\n\n## Transformation Patterns\n\n| Pattern | When | Benefit |\n| --- | --- | --- |\n| **map** | Simple transform | Direct |\n| **mapEffect** | Effects needed | Composable |\n| **filter** | Remove items | Selective |\n| **scan** | State + output | Stateful |\n| **chunk** | Batch processing | Efficient |\n\n---\n\n## See Also\n\n- [Stream Pattern 1: Map & Filter](./stream-pattern-map-filter-transformations.mdx) - Basic transforms\n- [Stream Pattern 4: Stateful Operations](./stream-pattern-stateful-operations.mdx) - State in streams\n- [Stream Pattern 7: Error Handling](./stream-pattern-error-handling.mdx) - Error transforms\n- [Concurrency Pattern 5: PubSub](./concurrency-pattern-pubsub-event-broadcast.mdx) - Event streams" - }, - { - "id": "stream-vs-effect", - "title": "Stream vs Effect - When to Use Which", - "description": "Use Effect for single values, Stream for sequences of values.", - "skillLevel": "beginner", - "useCases": [ - "streams-getting-started" - ], - "example": "```typescript\nimport { Effect, Stream } from \"effect\"\n\n// ============================================\n// EFFECT: Single result operations\n// ============================================\n\n// Fetch one user - returns Effect\nconst fetchUser = (id: string) =>\n Effect.tryPromise(() =>\n fetch(`/api/users/${id}`).then((r) => r.json())\n )\n\n// Read entire config - returns Effect\nconst loadConfig = Effect.tryPromise(() =>\n fetch(\"/config.json\").then((r) => r.json())\n)\n\n// ============================================\n// STREAM: Multiple values operations\n// ============================================\n\n// Process file line by line - returns Stream\nconst fileLines = Stream.fromIterable([\n \"line 1\",\n \"line 2\",\n \"line 3\",\n])\n\n// Generate events over time - returns Stream\nconst events = Stream.make(\n { type: \"click\", x: 10 },\n { type: \"click\", x: 20 },\n { type: \"scroll\", y: 100 },\n)\n\n// ============================================\n// CONVERTING BETWEEN THEM\n// ============================================\n\n// Effect → Stream (single value becomes 1-element stream)\nconst effectToStream = Stream.fromEffect(fetchUser(\"123\"))\n\n// Stream → Effect (collect all values into array)\nconst streamToEffect = Stream.runCollect(fileLines)\n\n// Stream → Effect (process each value for side effects)\nconst processAll = fileLines.pipe(\n Stream.runForEach((line) => Effect.log(`Processing: ${line}`))\n)\n\n// ============================================\n// DECISION GUIDE\n// ============================================\n\n// Use Effect when:\n// - Fetching a single resource\n// - Computing a single result\n// - Performing one action\n\n// Use Stream when:\n// - Reading files line by line\n// - Processing paginated API results\n// - Handling real-time events\n// - Processing large datasets\n// - Building data pipelines\n```", - "antiPattern": "", - "explanation": "Both Effect and Stream are lazy and composable, but they serve different purposes:\n\n| Aspect | Effect | Stream |\n|--------|--------|--------|\n| **Produces** | One value | Zero or more values |\n| **Memory** | Holds one result | Processes incrementally |\n| **Use case** | API call, DB query | File lines, events, batches |\n\n---", - "content": "## Guideline\n\nUse `Effect` when your operation produces a single result. Use `Stream` when your operation produces multiple values over time.\n\n---\n\n## Rationale\n\nBoth Effect and Stream are lazy and composable, but they serve different purposes:\n\n| Aspect | Effect | Stream |\n|--------|--------|--------|\n| **Produces** | One value | Zero or more values |\n| **Memory** | Holds one result | Processes incrementally |\n| **Use case** | API call, DB query | File lines, events, batches |\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream } from \"effect\"\n\n// ============================================\n// EFFECT: Single result operations\n// ============================================\n\n// Fetch one user - returns Effect\nconst fetchUser = (id: string) =>\n Effect.tryPromise(() =>\n fetch(`/api/users/${id}`).then((r) => r.json())\n )\n\n// Read entire config - returns Effect\nconst loadConfig = Effect.tryPromise(() =>\n fetch(\"/config.json\").then((r) => r.json())\n)\n\n// ============================================\n// STREAM: Multiple values operations\n// ============================================\n\n// Process file line by line - returns Stream\nconst fileLines = Stream.fromIterable([\n \"line 1\",\n \"line 2\",\n \"line 3\",\n])\n\n// Generate events over time - returns Stream\nconst events = Stream.make(\n { type: \"click\", x: 10 },\n { type: \"click\", x: 20 },\n { type: \"scroll\", y: 100 },\n)\n\n// ============================================\n// CONVERTING BETWEEN THEM\n// ============================================\n\n// Effect → Stream (single value becomes 1-element stream)\nconst effectToStream = Stream.fromEffect(fetchUser(\"123\"))\n\n// Stream → Effect (collect all values into array)\nconst streamToEffect = Stream.runCollect(fileLines)\n\n// Stream → Effect (process each value for side effects)\nconst processAll = fileLines.pipe(\n Stream.runForEach((line) => Effect.log(`Processing: ${line}`))\n)\n\n// ============================================\n// DECISION GUIDE\n// ============================================\n\n// Use Effect when:\n// - Fetching a single resource\n// - Computing a single result\n// - Performing one action\n\n// Use Stream when:\n// - Reading files line by line\n// - Processing paginated API results\n// - Handling real-time events\n// - Processing large datasets\n// - Building data pipelines\n```\n\n## Decision Flowchart\n\n```\nDoes your operation produce multiple values?\n├── No → Use Effect\n└── Yes → Do they arrive over time or need lazy processing?\n ├── No, all at once → Use Effect with Array\n └── Yes → Use Stream\n```\n\n## Common Conversions\n\n| From | To | Method |\n|------|-----|--------|\n| Effect → Stream | `Stream.fromEffect(effect)` |\n| Stream → Effect | `Stream.runCollect(stream)` |\n| Stream → Effect | `Stream.runForEach(stream, fn)` |\n| Array → Stream | `Stream.fromIterable(array)` |\n| Stream → Array | `Stream.runCollect` then spread |" - }, - { - "id": "supercharge-your-editor-with-the-effect-lsp", - "title": "Supercharge Your Editor with the Effect LSP", - "description": "Install and use the Effect LSP extension for enhanced type information and error checking in your editor.", - "skillLevel": "intermediate", - "useCases": [ - "tooling-and-debugging" - ], - "example": "Imagine you have the following code. Without the LSP, hovering over `program` might show a complex, hard-to-read inferred type.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define Logger service using Effect.Service pattern\nclass Logger extends Effect.Service()(\"Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.log(`LOG: ${msg}`),\n }),\n}) {}\n\nconst program = Effect.succeed(42).pipe(\n Effect.map((n) => n.toString()),\n Effect.flatMap((s) => Effect.log(s)),\n Effect.provide(Logger.Default)\n);\n\n// Run the program\nEffect.runPromise(program);\n```\n\nWith the Effect LSP installed, your editor would display a clear, readable overlay right above the `program` variable, looking something like this:\n\n```\n// (LSP Inlay Hint)\n// program: Effect\n```\n\nThis 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.\n\n---", - "antiPattern": "Going without the LSP. While your code will still compile and work perfectly fine, you are essentially \"flying blind.\" You miss out on the rich, real-time feedback that the LSP provides, forcing you to rely more heavily on manual type checking, `tsc` runs, and deciphering complex inferred types from your editor's default tooltips. This leads to a slower, less efficient development cycle.", - "explanation": "Effect's type system is incredibly powerful, but TypeScript's default language server doesn't always display the rich information contained within the `A`, `E`, and `R` channels in the most intuitive way.\n\nThe Effect LSP is a specialized tool that understands the semantics of Effect. It hooks into your editor to provide a superior experience:\n\n- **Rich Inline Types:** It displays the full `Effect` signature directly in your code as you work, so you always know exactly what an effect produces, how it can fail, and what it requires.\n- **Clear Error Messages:** It provides more specific and helpful error messages tailored to Effect's APIs.\n- **Enhanced Autocompletion:** It can offer more context-aware suggestions.\n\nThis tool essentially makes the compiler's knowledge visible at a glance, reducing the mental overhead of tracking complex types and allowing you to catch errors before you even save the file.\n\n---", - "content": "## Guideline\n\nTo significantly improve your development experience with Effect, install the official **Effect Language Server (LSP)** extension for your code editor (e.g., the \"Effect\" extension in VS Code).\n\n---\n\n## Rationale\n\nEffect's type system is incredibly powerful, but TypeScript's default language server doesn't always display the rich information contained within the `A`, `E`, and `R` channels in the most intuitive way.\n\nThe Effect LSP is a specialized tool that understands the semantics of Effect. It hooks into your editor to provide a superior experience:\n\n- **Rich Inline Types:** It displays the full `Effect` signature directly in your code as you work, so you always know exactly what an effect produces, how it can fail, and what it requires.\n- **Clear Error Messages:** It provides more specific and helpful error messages tailored to Effect's APIs.\n- **Enhanced Autocompletion:** It can offer more context-aware suggestions.\n\nThis tool essentially makes the compiler's knowledge visible at a glance, reducing the mental overhead of tracking complex types and allowing you to catch errors before you even save the file.\n\n---\n\n## Good Example\n\nImagine you have the following code. Without the LSP, hovering over `program` might show a complex, hard-to-read inferred type.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define Logger service using Effect.Service pattern\nclass Logger extends Effect.Service()(\"Logger\", {\n sync: () => ({\n log: (msg: string) => Effect.log(`LOG: ${msg}`),\n }),\n}) {}\n\nconst program = Effect.succeed(42).pipe(\n Effect.map((n) => n.toString()),\n Effect.flatMap((s) => Effect.log(s)),\n Effect.provide(Logger.Default)\n);\n\n// Run the program\nEffect.runPromise(program);\n```\n\nWith the Effect LSP installed, your editor would display a clear, readable overlay right above the `program` variable, looking something like this:\n\n```\n// (LSP Inlay Hint)\n// program: Effect\n```\n\nThis 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.\n\n---\n\n## Anti-Pattern\n\nGoing without the LSP. While your code will still compile and work perfectly fine, you are essentially \"flying blind.\" You miss out on the rich, real-time feedback that the LSP provides, forcing you to rely more heavily on manual type checking, `tsc` runs, and deciphering complex inferred types from your editor's default tooltips. This leads to a slower, less efficient development cycle." - }, - { - "id": "stream-take-drop", - "title": "Take and Drop Stream Elements", - "description": "Use take/drop to control stream size, takeWhile/dropWhile for conditional limits.", - "skillLevel": "beginner", - "useCases": [ - "streams-getting-started" - ], - "example": "```typescript\nimport { Effect, Stream } from \"effect\"\n\nconst numbers = Stream.make(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\n// ============================================\n// take - Get first N elements\n// ============================================\n\nconst firstThree = numbers.pipe(\n Stream.take(3),\n Stream.runCollect\n)\n\nEffect.runPromise(firstThree).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3]\n})\n\n// ============================================\n// drop - Skip first N elements\n// ============================================\n\nconst skipThree = numbers.pipe(\n Stream.drop(3),\n Stream.runCollect\n)\n\nEffect.runPromise(skipThree).then((chunk) => {\n console.log([...chunk]) // [4, 5, 6, 7, 8, 9, 10]\n})\n\n// ============================================\n// Combine for pagination (skip + limit)\n// ============================================\n\nconst page2 = numbers.pipe(\n Stream.drop(3), // Skip first page\n Stream.take(3), // Take second page\n Stream.runCollect\n)\n\nEffect.runPromise(page2).then((chunk) => {\n console.log([...chunk]) // [4, 5, 6]\n})\n\n// ============================================\n// takeWhile - Take while condition is true\n// ============================================\n\nconst untilFive = numbers.pipe(\n Stream.takeWhile((n) => n < 5),\n Stream.runCollect\n)\n\nEffect.runPromise(untilFive).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3, 4]\n})\n\n// ============================================\n// dropWhile - Skip while condition is true\n// ============================================\n\nconst afterFive = numbers.pipe(\n Stream.dropWhile((n) => n < 5),\n Stream.runCollect\n)\n\nEffect.runPromise(afterFive).then((chunk) => {\n console.log([...chunk]) // [5, 6, 7, 8, 9, 10]\n})\n\n// ============================================\n// takeUntil - Take until condition becomes true\n// ============================================\n\nconst untilSix = numbers.pipe(\n Stream.takeUntil((n) => n === 6),\n Stream.runCollect\n)\n\nEffect.runPromise(untilSix).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3, 4, 5, 6]\n})\n\n// ============================================\n// Practical: Process file with header\n// ============================================\n\nconst fileLines = Stream.make(\n \"# Header\",\n \"# Comment\",\n \"data1\",\n \"data2\",\n \"data3\"\n)\n\nconst dataOnly = fileLines.pipe(\n Stream.dropWhile((line) => line.startsWith(\"#\")),\n Stream.runCollect\n)\n\nEffect.runPromise(dataOnly).then((chunk) => {\n console.log([...chunk]) // [\"data1\", \"data2\", \"data3\"]\n})\n```", - "antiPattern": "", - "explanation": "Streams can be infinite or very large. These operators let you:\n\n1. **Limit processing** - Only take what you need\n2. **Skip headers** - Drop first N elements\n3. **Conditional limits** - Take/drop based on predicates\n4. **Pagination** - Implement skip/limit patterns\n\n---", - "content": "## Guideline\n\nUse `take` to limit how many elements to process. Use `drop` to skip elements. Add `While` variants for condition-based limits.\n\n---\n\n## Rationale\n\nStreams can be infinite or very large. These operators let you:\n\n1. **Limit processing** - Only take what you need\n2. **Skip headers** - Drop first N elements\n3. **Conditional limits** - Take/drop based on predicates\n4. **Pagination** - Implement skip/limit patterns\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream } from \"effect\"\n\nconst numbers = Stream.make(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)\n\n// ============================================\n// take - Get first N elements\n// ============================================\n\nconst firstThree = numbers.pipe(\n Stream.take(3),\n Stream.runCollect\n)\n\nEffect.runPromise(firstThree).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3]\n})\n\n// ============================================\n// drop - Skip first N elements\n// ============================================\n\nconst skipThree = numbers.pipe(\n Stream.drop(3),\n Stream.runCollect\n)\n\nEffect.runPromise(skipThree).then((chunk) => {\n console.log([...chunk]) // [4, 5, 6, 7, 8, 9, 10]\n})\n\n// ============================================\n// Combine for pagination (skip + limit)\n// ============================================\n\nconst page2 = numbers.pipe(\n Stream.drop(3), // Skip first page\n Stream.take(3), // Take second page\n Stream.runCollect\n)\n\nEffect.runPromise(page2).then((chunk) => {\n console.log([...chunk]) // [4, 5, 6]\n})\n\n// ============================================\n// takeWhile - Take while condition is true\n// ============================================\n\nconst untilFive = numbers.pipe(\n Stream.takeWhile((n) => n < 5),\n Stream.runCollect\n)\n\nEffect.runPromise(untilFive).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3, 4]\n})\n\n// ============================================\n// dropWhile - Skip while condition is true\n// ============================================\n\nconst afterFive = numbers.pipe(\n Stream.dropWhile((n) => n < 5),\n Stream.runCollect\n)\n\nEffect.runPromise(afterFive).then((chunk) => {\n console.log([...chunk]) // [5, 6, 7, 8, 9, 10]\n})\n\n// ============================================\n// takeUntil - Take until condition becomes true\n// ============================================\n\nconst untilSix = numbers.pipe(\n Stream.takeUntil((n) => n === 6),\n Stream.runCollect\n)\n\nEffect.runPromise(untilSix).then((chunk) => {\n console.log([...chunk]) // [1, 2, 3, 4, 5, 6]\n})\n\n// ============================================\n// Practical: Process file with header\n// ============================================\n\nconst fileLines = Stream.make(\n \"# Header\",\n \"# Comment\",\n \"data1\",\n \"data2\",\n \"data3\"\n)\n\nconst dataOnly = fileLines.pipe(\n Stream.dropWhile((line) => line.startsWith(\"#\")),\n Stream.runCollect\n)\n\nEffect.runPromise(dataOnly).then((chunk) => {\n console.log([...chunk]) // [\"data1\", \"data2\", \"data3\"]\n})\n```\n\n## Quick Reference\n\n| Operator | Behavior |\n|----------|----------|\n| **take(n)** | First N elements only |\n| **drop(n)** | Skip first N elements |\n| **takeWhile(pred)** | Take while predicate is true |\n| **dropWhile(pred)** | Skip while predicate is true |\n| **takeUntil(pred)** | Take until predicate becomes true (inclusive) |\n| **dropUntil(pred)** | Skip until predicate becomes true |\n\n## Pagination Pattern\n\n```typescript\nconst getPage = (pageNum: number, pageSize: number) =>\n allItems.pipe(\n Stream.drop(pageNum * pageSize),\n Stream.take(pageSize),\n Stream.runCollect\n )\n```" - }, - { - "id": "teach-your-ai-agents-effect-with-the-mcp-server", - "title": "Teach your AI Agents Effect with the MCP Server", - "description": "Use the MCP server to provide live application context to AI coding agents, enabling more accurate assistance.", - "skillLevel": "advanced", - "useCases": [ - "tooling-and-debugging" - ], - "example": "The \"Good Example\" is the workflow this pattern enables.\n\n1. **You run the MCP server** in your terminal, pointing it at your main `AppLayer`.\n\n ```bash\n npx @effect/mcp-server --layer src/layers.ts:AppLayer\n ```\n\n2. **You configure your AI agent** (e.g., Cursor) to use the MCP server's endpoint (`http://localhost:3333`).\n\n3. **You ask the AI a question** that requires deep context about your app:\n\n > \"Refactor this code to use the `UserService` to fetch a user by ID and log the result with the `Logger`.\"\n\n4. **The AI, in the background, queries the MCP server:**\n\n - It discovers that `UserService` and `Logger` are available in the `AppLayer`.\n - It retrieves the exact method signature for `UserService.getUser` and `Logger.log`.\n\n5. **The AI generates correct, context-aware code** because it's not guessing; it's using the live architectural information provided by the MCP server.\n\n```typescript\n// The AI generates this correct code:\nimport { Effect } from \"effect\";\nimport { UserService } from \"./features/User/UserService.js\";\nconst program = Effect.gen(function* () {\n const userService = yield* UserService;\n\n const user = yield* userService.getUser(\"123\");\n yield* Effect.log(`Found user: ${user.name}`);\n});\n```\n\n---", - "antiPattern": "Working with an AI agent without providing it with specific context. The agent will be forced to guess based on open files or generic knowledge. This often leads to it hallucinating method names, getting dependency injection wrong, or failing to handle specific error types, requiring you to manually correct its output and defeating the purpose of using an AI assistant.", - "explanation": "AI coding agents are powerful, but they often lack the deep, structural understanding of a complex Effect application. They might not know which services are available in the context, what a specific `Layer` provides, or how your feature modules are composed.\n\nThe MCP server solves this problem. It's a specialized server that runs alongside your application during development. It inspects your `AppLayer` and creates a real-time, queryable model of your entire application architecture.\n\nAn AI agent can then connect to this MCP server to ask specific questions before generating code, such as:\n\n- \"What services are available in the current context?\"\n- \"What is the full API of the `UserService`?\"\n- \"What errors can `UserRepository.findById` fail with?\"\n\nBy providing this live, ground-truth context, you transform your AI from a generic coding assistant into a specialized expert on _your_ specific codebase, resulting in far more accurate and useful code generation and refactoring.\n\n---", - "content": "## Guideline\n\nTo enable AI coding agents (like Cursor or custom bots) to provide highly accurate, context-aware assistance for your Effect application, run the **Effect MCP (Meta-Circular-Protocol) server**. This tool exposes your application's entire dependency graph and service structure in a machine-readable format.\n\n---\n\n## Rationale\n\nAI coding agents are powerful, but they often lack the deep, structural understanding of a complex Effect application. They might not know which services are available in the context, what a specific `Layer` provides, or how your feature modules are composed.\n\nThe MCP server solves this problem. It's a specialized server that runs alongside your application during development. It inspects your `AppLayer` and creates a real-time, queryable model of your entire application architecture.\n\nAn AI agent can then connect to this MCP server to ask specific questions before generating code, such as:\n\n- \"What services are available in the current context?\"\n- \"What is the full API of the `UserService`?\"\n- \"What errors can `UserRepository.findById` fail with?\"\n\nBy providing this live, ground-truth context, you transform your AI from a generic coding assistant into a specialized expert on _your_ specific codebase, resulting in far more accurate and useful code generation and refactoring.\n\n---\n\n## Good Example\n\nThe \"Good Example\" is the workflow this pattern enables.\n\n1. **You run the MCP server** in your terminal, pointing it at your main `AppLayer`.\n\n ```bash\n npx @effect/mcp-server --layer src/layers.ts:AppLayer\n ```\n\n2. **You configure your AI agent** (e.g., Cursor) to use the MCP server's endpoint (`http://localhost:3333`).\n\n3. **You ask the AI a question** that requires deep context about your app:\n\n > \"Refactor this code to use the `UserService` to fetch a user by ID and log the result with the `Logger`.\"\n\n4. **The AI, in the background, queries the MCP server:**\n\n - It discovers that `UserService` and `Logger` are available in the `AppLayer`.\n - It retrieves the exact method signature for `UserService.getUser` and `Logger.log`.\n\n5. **The AI generates correct, context-aware code** because it's not guessing; it's using the live architectural information provided by the MCP server.\n\n```typescript\n// The AI generates this correct code:\nimport { Effect } from \"effect\";\nimport { UserService } from \"./features/User/UserService.js\";\nconst program = Effect.gen(function* () {\n const userService = yield* UserService;\n\n const user = yield* userService.getUser(\"123\");\n yield* Effect.log(`Found user: ${user.name}`);\n});\n```\n\n---\n\n## Anti-Pattern\n\nWorking with an AI agent without providing it with specific context. The agent will be forced to guess based on open files or generic knowledge. This often leads to it hallucinating method names, getting dependency injection wrong, or failing to handle specific error types, requiring you to manually correct its output and defeating the purpose of using an AI assistant." - }, - { - "id": "testing-concurrent-code", - "title": "Test Concurrent Code", - "description": "Use TestClock and controlled concurrency to make concurrent tests deterministic.", - "skillLevel": "advanced", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Fiber, Ref, TestClock, Duration, Deferred } from \"effect\"\n\ndescribe(\"Concurrent Code Testing\", () => {\n // ============================================\n // 1. Test parallel execution\n // ============================================\n\n it(\"should run effects in parallel\", async () => {\n const executionOrder: string[] = []\n\n const task1 = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n executionOrder.push(\"task1\")\n return 1\n })\n\n const task2 = Effect.gen(function* () {\n yield* Effect.sleep(\"50 millis\")\n executionOrder.push(\"task2\")\n return 2\n })\n\n const program = Effect.all([task1, task2], { concurrency: 2 })\n\n // Use TestClock to control time\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const fiber = yield* Effect.fork(program)\n\n // Advance time to trigger both tasks\n yield* TestClock.adjust(\"100 millis\")\n\n return yield* Fiber.join(fiber)\n }).pipe(Effect.provide(TestClock.live))\n )\n\n expect(result).toEqual([1, 2])\n // With real time, task2 would complete first\n expect(executionOrder).toContain(\"task1\")\n expect(executionOrder).toContain(\"task2\")\n })\n\n // ============================================\n // 2. Test race conditions\n // ============================================\n\n it(\"should handle race condition correctly\", async () => {\n const counter = await Effect.runPromise(\n Effect.gen(function* () {\n const ref = yield* Ref.make(0)\n\n // Simulate concurrent increments\n const increment = Ref.update(ref, (n) => n + 1)\n\n // Run 100 concurrent increments\n yield* Effect.all(\n Array.from({ length: 100 }, () => increment),\n { concurrency: \"unbounded\" }\n )\n\n return yield* Ref.get(ref)\n })\n )\n\n // Ref is atomic, so all increments should be counted\n expect(counter).toBe(100)\n })\n\n // ============================================\n // 3. Test with controlled fiber execution\n // ============================================\n\n it(\"should test fiber lifecycle\", async () => {\n const events: string[] = []\n\n const program = Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n events.push(\"started\")\n yield* Effect.sleep(\"1 second\")\n events.push(\"completed\")\n return \"result\"\n })\n )\n\n events.push(\"forked\")\n\n // Interrupt the fiber\n yield* Fiber.interrupt(fiber)\n events.push(\"interrupted\")\n\n const exit = yield* Fiber.await(fiber)\n return exit\n })\n\n await Effect.runPromise(program)\n\n expect(events).toEqual([\"forked\", \"started\", \"interrupted\"])\n expect(events).not.toContain(\"completed\")\n })\n\n // ============================================\n // 4. Test timeout behavior\n // ============================================\n\n it(\"should timeout slow operations\", async () => {\n const slowOperation = Effect.gen(function* () {\n yield* Effect.sleep(\"10 seconds\")\n return \"completed\"\n })\n\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n slowOperation.pipe(Effect.timeout(\"1 second\"))\n )\n\n // Advance past the timeout\n yield* TestClock.adjust(\"2 seconds\")\n\n return yield* Fiber.join(fiber)\n }).pipe(Effect.provide(TestClock.live))\n )\n\n // Result is Option.None due to timeout\n expect(result._tag).toBe(\"None\")\n })\n\n // ============================================\n // 5. Test with Deferred for synchronization\n // ============================================\n\n it(\"should synchronize fibers correctly\", async () => {\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const deferred = yield* Deferred.make()\n const results: string[] = []\n\n // Consumer waits for producer\n const consumer = Effect.fork(\n Effect.gen(function* () {\n const value = yield* Deferred.await(deferred)\n results.push(`consumed: ${value}`)\n })\n )\n\n // Producer completes the deferred\n const producer = Effect.gen(function* () {\n results.push(\"producing\")\n yield* Deferred.succeed(deferred, \"data\")\n results.push(\"produced\")\n })\n\n yield* consumer\n yield* producer\n\n // Wait for consumer to process\n yield* Effect.sleep(\"10 millis\")\n\n return results\n })\n )\n\n expect(result).toContain(\"producing\")\n expect(result).toContain(\"produced\")\n expect(result).toContain(\"consumed: data\")\n })\n\n // ============================================\n // 6. Test for absence of deadlocks\n // ============================================\n\n it(\"should not deadlock with proper resource ordering\", async () => {\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const ref1 = yield* Ref.make(0)\n const ref2 = yield* Ref.make(0)\n\n // Two fibers accessing refs in same order (no deadlock)\n const fiber1 = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Ref.update(ref1, (n) => n + 1)\n yield* Ref.update(ref2, (n) => n + 1)\n })\n )\n\n const fiber2 = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Ref.update(ref1, (n) => n + 1)\n yield* Ref.update(ref2, (n) => n + 1)\n })\n )\n\n yield* Fiber.join(fiber1)\n yield* Fiber.join(fiber2)\n\n return [yield* Ref.get(ref1), yield* Ref.get(ref2)]\n }).pipe(Effect.timeout(\"1 second\"))\n )\n\n expect(result._tag).toBe(\"Some\")\n expect(result.value).toEqual([2, 2])\n })\n})\n```", - "antiPattern": "", - "explanation": "Concurrent code is hard to test:\n\n1. **Non-determinism** - Different runs, different results\n2. **Race conditions** - Timing-dependent bugs\n3. **Deadlocks** - Hard to reproduce\n4. **Flaky tests** - Pass sometimes, fail others\n\nEffect's test utilities provide control over timing and concurrency.\n\n---", - "content": "## Guideline\n\nUse Effect's TestClock and fiber control to make concurrent tests deterministic and repeatable.\n\n---\n\n## Rationale\n\nConcurrent code is hard to test:\n\n1. **Non-determinism** - Different runs, different results\n2. **Race conditions** - Timing-dependent bugs\n3. **Deadlocks** - Hard to reproduce\n4. **Flaky tests** - Pass sometimes, fail others\n\nEffect's test utilities provide control over timing and concurrency.\n\n---\n\n## Good Example\n\n```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Fiber, Ref, TestClock, Duration, Deferred } from \"effect\"\n\ndescribe(\"Concurrent Code Testing\", () => {\n // ============================================\n // 1. Test parallel execution\n // ============================================\n\n it(\"should run effects in parallel\", async () => {\n const executionOrder: string[] = []\n\n const task1 = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n executionOrder.push(\"task1\")\n return 1\n })\n\n const task2 = Effect.gen(function* () {\n yield* Effect.sleep(\"50 millis\")\n executionOrder.push(\"task2\")\n return 2\n })\n\n const program = Effect.all([task1, task2], { concurrency: 2 })\n\n // Use TestClock to control time\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const fiber = yield* Effect.fork(program)\n\n // Advance time to trigger both tasks\n yield* TestClock.adjust(\"100 millis\")\n\n return yield* Fiber.join(fiber)\n }).pipe(Effect.provide(TestClock.live))\n )\n\n expect(result).toEqual([1, 2])\n // With real time, task2 would complete first\n expect(executionOrder).toContain(\"task1\")\n expect(executionOrder).toContain(\"task2\")\n })\n\n // ============================================\n // 2. Test race conditions\n // ============================================\n\n it(\"should handle race condition correctly\", async () => {\n const counter = await Effect.runPromise(\n Effect.gen(function* () {\n const ref = yield* Ref.make(0)\n\n // Simulate concurrent increments\n const increment = Ref.update(ref, (n) => n + 1)\n\n // Run 100 concurrent increments\n yield* Effect.all(\n Array.from({ length: 100 }, () => increment),\n { concurrency: \"unbounded\" }\n )\n\n return yield* Ref.get(ref)\n })\n )\n\n // Ref is atomic, so all increments should be counted\n expect(counter).toBe(100)\n })\n\n // ============================================\n // 3. Test with controlled fiber execution\n // ============================================\n\n it(\"should test fiber lifecycle\", async () => {\n const events: string[] = []\n\n const program = Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n events.push(\"started\")\n yield* Effect.sleep(\"1 second\")\n events.push(\"completed\")\n return \"result\"\n })\n )\n\n events.push(\"forked\")\n\n // Interrupt the fiber\n yield* Fiber.interrupt(fiber)\n events.push(\"interrupted\")\n\n const exit = yield* Fiber.await(fiber)\n return exit\n })\n\n await Effect.runPromise(program)\n\n expect(events).toEqual([\"forked\", \"started\", \"interrupted\"])\n expect(events).not.toContain(\"completed\")\n })\n\n // ============================================\n // 4. Test timeout behavior\n // ============================================\n\n it(\"should timeout slow operations\", async () => {\n const slowOperation = Effect.gen(function* () {\n yield* Effect.sleep(\"10 seconds\")\n return \"completed\"\n })\n\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n slowOperation.pipe(Effect.timeout(\"1 second\"))\n )\n\n // Advance past the timeout\n yield* TestClock.adjust(\"2 seconds\")\n\n return yield* Fiber.join(fiber)\n }).pipe(Effect.provide(TestClock.live))\n )\n\n // Result is Option.None due to timeout\n expect(result._tag).toBe(\"None\")\n })\n\n // ============================================\n // 5. Test with Deferred for synchronization\n // ============================================\n\n it(\"should synchronize fibers correctly\", async () => {\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const deferred = yield* Deferred.make()\n const results: string[] = []\n\n // Consumer waits for producer\n const consumer = Effect.fork(\n Effect.gen(function* () {\n const value = yield* Deferred.await(deferred)\n results.push(`consumed: ${value}`)\n })\n )\n\n // Producer completes the deferred\n const producer = Effect.gen(function* () {\n results.push(\"producing\")\n yield* Deferred.succeed(deferred, \"data\")\n results.push(\"produced\")\n })\n\n yield* consumer\n yield* producer\n\n // Wait for consumer to process\n yield* Effect.sleep(\"10 millis\")\n\n return results\n })\n )\n\n expect(result).toContain(\"producing\")\n expect(result).toContain(\"produced\")\n expect(result).toContain(\"consumed: data\")\n })\n\n // ============================================\n // 6. Test for absence of deadlocks\n // ============================================\n\n it(\"should not deadlock with proper resource ordering\", async () => {\n const result = await Effect.runPromise(\n Effect.gen(function* () {\n const ref1 = yield* Ref.make(0)\n const ref2 = yield* Ref.make(0)\n\n // Two fibers accessing refs in same order (no deadlock)\n const fiber1 = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Ref.update(ref1, (n) => n + 1)\n yield* Ref.update(ref2, (n) => n + 1)\n })\n )\n\n const fiber2 = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Ref.update(ref1, (n) => n + 1)\n yield* Ref.update(ref2, (n) => n + 1)\n })\n )\n\n yield* Fiber.join(fiber1)\n yield* Fiber.join(fiber2)\n\n return [yield* Ref.get(ref1), yield* Ref.get(ref2)]\n }).pipe(Effect.timeout(\"1 second\"))\n )\n\n expect(result._tag).toBe(\"Some\")\n expect(result.value).toEqual([2, 2])\n })\n})\n```\n\n## Testing Strategies\n\n| Scenario | Approach |\n|----------|----------|\n| Race conditions | Use Ref (atomic) and verify counts |\n| Timing | Use TestClock to control time |\n| Fiber lifecycle | Fork, interrupt, join |\n| Synchronization | Use Deferred |\n| Deadlocks | Timeout tests |\n\n## Key Test Utilities\n\n| Utility | Purpose |\n|---------|---------|\n| `TestClock` | Control time in tests |\n| `Fiber.fork/join` | Control fiber execution |\n| `Deferred` | Synchronize between fibers |\n| `Ref` | Atomic state |\n| `Effect.timeout` | Detect hangs |" - }, - { - "id": "testing-with-services", - "title": "Test Effects with Services", - "description": "Provide test implementations of services to make Effect programs testable.", - "skillLevel": "beginner", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Context } from \"effect\"\n\n// ============================================\n// 1. Define a service\n// ============================================\n\nclass UserRepository extends Context.Tag(\"UserRepository\")<\n UserRepository,\n {\n readonly findById: (id: string) => Effect.Effect\n readonly save: (user: User) => Effect.Effect\n }\n>() {}\n\ninterface User {\n id: string\n name: string\n email: string\n}\n\n// ============================================\n// 2. Code that uses the service\n// ============================================\n\nconst getUser = (id: string) =>\n Effect.gen(function* () {\n const repo = yield* UserRepository\n const user = yield* repo.findById(id)\n \n if (!user) {\n return yield* Effect.fail(new Error(`User ${id} not found`))\n }\n \n return user\n })\n\nconst createUser = (name: string, email: string) =>\n Effect.gen(function* () {\n const repo = yield* UserRepository\n \n const user: User = {\n id: crypto.randomUUID(),\n name,\n email,\n }\n \n yield* repo.save(user)\n return user\n })\n\n// ============================================\n// 3. Create a test implementation\n// ============================================\n\nconst makeTestUserRepository = (initialUsers: User[] = []) => {\n const users = new Map(initialUsers.map(u => [u.id, u]))\n \n return UserRepository.of({\n findById: (id) => Effect.succeed(users.get(id) ?? null),\n save: (user) => Effect.sync(() => { users.set(user.id, user) }),\n })\n}\n\n// ============================================\n// 4. Write tests\n// ============================================\n\ndescribe(\"User Service Tests\", () => {\n it(\"should find an existing user\", async () => {\n const testUser: User = {\n id: \"123\",\n name: \"Alice\",\n email: \"alice@example.com\",\n }\n \n const testRepo = makeTestUserRepository([testUser])\n \n const result = await Effect.runPromise(\n getUser(\"123\").pipe(\n Effect.provideService(UserRepository, testRepo)\n )\n )\n \n expect(result).toEqual(testUser)\n })\n\n it(\"should fail when user not found\", async () => {\n const testRepo = makeTestUserRepository([])\n \n await expect(\n Effect.runPromise(\n getUser(\"999\").pipe(\n Effect.provideService(UserRepository, testRepo)\n )\n )\n ).rejects.toThrow(\"User 999 not found\")\n })\n\n it(\"should create and save a user\", async () => {\n const savedUsers: User[] = []\n \n const trackingRepo = UserRepository.of({\n findById: () => Effect.succeed(null),\n save: (user) => Effect.sync(() => { savedUsers.push(user) }),\n })\n \n const result = await Effect.runPromise(\n createUser(\"Bob\", \"bob@example.com\").pipe(\n Effect.provideService(UserRepository, trackingRepo)\n )\n )\n \n expect(result.name).toBe(\"Bob\")\n expect(result.email).toBe(\"bob@example.com\")\n expect(savedUsers).toHaveLength(1)\n expect(savedUsers[0].name).toBe(\"Bob\")\n })\n})\n```", - "antiPattern": "", - "explanation": "Effect's service pattern makes testing easy:\n\n1. **Declare dependencies** - Effects specify what they need\n2. **Inject test doubles** - Provide fake implementations for tests\n3. **No mocking libraries** - Just provide different service implementations\n4. **Type-safe** - Compiler ensures you provide all dependencies\n\n---", - "content": "## Guideline\n\nWhen testing Effects that require services, provide test implementations using `Effect.provideService` or test layers.\n\n---\n\n## Rationale\n\nEffect's service pattern makes testing easy:\n\n1. **Declare dependencies** - Effects specify what they need\n2. **Inject test doubles** - Provide fake implementations for tests\n3. **No mocking libraries** - Just provide different service implementations\n4. **Type-safe** - Compiler ensures you provide all dependencies\n\n---\n\n## Good Example\n\n```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Context } from \"effect\"\n\n// ============================================\n// 1. Define a service\n// ============================================\n\nclass UserRepository extends Context.Tag(\"UserRepository\")<\n UserRepository,\n {\n readonly findById: (id: string) => Effect.Effect\n readonly save: (user: User) => Effect.Effect\n }\n>() {}\n\ninterface User {\n id: string\n name: string\n email: string\n}\n\n// ============================================\n// 2. Code that uses the service\n// ============================================\n\nconst getUser = (id: string) =>\n Effect.gen(function* () {\n const repo = yield* UserRepository\n const user = yield* repo.findById(id)\n \n if (!user) {\n return yield* Effect.fail(new Error(`User ${id} not found`))\n }\n \n return user\n })\n\nconst createUser = (name: string, email: string) =>\n Effect.gen(function* () {\n const repo = yield* UserRepository\n \n const user: User = {\n id: crypto.randomUUID(),\n name,\n email,\n }\n \n yield* repo.save(user)\n return user\n })\n\n// ============================================\n// 3. Create a test implementation\n// ============================================\n\nconst makeTestUserRepository = (initialUsers: User[] = []) => {\n const users = new Map(initialUsers.map(u => [u.id, u]))\n \n return UserRepository.of({\n findById: (id) => Effect.succeed(users.get(id) ?? null),\n save: (user) => Effect.sync(() => { users.set(user.id, user) }),\n })\n}\n\n// ============================================\n// 4. Write tests\n// ============================================\n\ndescribe(\"User Service Tests\", () => {\n it(\"should find an existing user\", async () => {\n const testUser: User = {\n id: \"123\",\n name: \"Alice\",\n email: \"alice@example.com\",\n }\n \n const testRepo = makeTestUserRepository([testUser])\n \n const result = await Effect.runPromise(\n getUser(\"123\").pipe(\n Effect.provideService(UserRepository, testRepo)\n )\n )\n \n expect(result).toEqual(testUser)\n })\n\n it(\"should fail when user not found\", async () => {\n const testRepo = makeTestUserRepository([])\n \n await expect(\n Effect.runPromise(\n getUser(\"999\").pipe(\n Effect.provideService(UserRepository, testRepo)\n )\n )\n ).rejects.toThrow(\"User 999 not found\")\n })\n\n it(\"should create and save a user\", async () => {\n const savedUsers: User[] = []\n \n const trackingRepo = UserRepository.of({\n findById: () => Effect.succeed(null),\n save: (user) => Effect.sync(() => { savedUsers.push(user) }),\n })\n \n const result = await Effect.runPromise(\n createUser(\"Bob\", \"bob@example.com\").pipe(\n Effect.provideService(UserRepository, trackingRepo)\n )\n )\n \n expect(result.name).toBe(\"Bob\")\n expect(result.email).toBe(\"bob@example.com\")\n expect(savedUsers).toHaveLength(1)\n expect(savedUsers[0].name).toBe(\"Bob\")\n })\n})\n```\n\n## Test Double Strategies\n\n| Strategy | When to Use |\n|----------|-------------|\n| **In-memory store** | Testing data operations |\n| **Tracking wrapper** | Verify methods were called |\n| **Stub returns** | Fixed responses for specific scenarios |\n| **Failing service** | Test error handling paths |\n\n## Key Points\n\n1. **Services are interfaces** - Easy to provide alternative implementations\n2. **No global mocks** - Each test provides its own dependencies\n3. **Type-safe** - Compiler ensures test doubles match service interface\n4. **Isolated tests** - Each test has its own service instance" - }, - { - "id": "testing-streams", - "title": "Test Streaming Effects", - "description": "Use Stream.runCollect and assertions to verify stream behavior.", - "skillLevel": "advanced", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Stream, Chunk, Ref } from \"effect\"\n\ndescribe(\"Stream Testing\", () => {\n // ============================================\n // 1. Test basic stream operations\n // ============================================\n\n it(\"should transform stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.map((n) => n * 2),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6, 8, 10])\n })\n\n it(\"should filter stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5, 6]).pipe(\n Stream.filter((n) => n % 2 === 0),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6])\n })\n\n // ============================================\n // 2. Test stream aggregation\n // ============================================\n\n it(\"should fold stream to single value\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.runFold(0, (acc, n) => acc + n)\n )\n )\n\n expect(result).toBe(15)\n })\n\n it(\"should count stream elements\", async () => {\n const count = await Effect.runPromise(\n Stream.fromIterable([\"a\", \"b\", \"c\", \"d\"]).pipe(\n Stream.runCount\n )\n )\n\n expect(count).toBe(4)\n })\n\n // ============================================\n // 3. Test error handling in streams\n // ============================================\n\n it(\"should catch errors in stream\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n Stream.mapEffect((n) =>\n n === 2\n ? Effect.fail(new Error(\"Failed on 2\"))\n : Effect.succeed(n * 10)\n ),\n Stream.catchAll((error) =>\n Stream.succeed(-1) // Replace error with sentinel\n ),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([10, -1])\n })\n\n it(\"should handle errors and continue with orElse\", async () => {\n const failingStream = Stream.fail(new Error(\"Primary failed\"))\n const fallbackStream = Stream.fromIterable([1, 2, 3])\n\n const result = await Effect.runPromise(\n failingStream.pipe(\n Stream.orElse(() => fallbackStream),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])\n })\n\n // ============================================\n // 4. Test stream chunking\n // ============================================\n\n it(\"should chunk stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.grouped(2),\n Stream.runCollect\n )\n )\n\n const chunks = Chunk.toReadonlyArray(result).map(Chunk.toReadonlyArray)\n expect(chunks).toEqual([[1, 2], [3, 4], [5]])\n })\n\n // ============================================\n // 5. Test stream with effects\n // ============================================\n\n it(\"should run effects for each element\", async () => {\n const processed: number[] = []\n\n await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n Stream.tap((n) =>\n Effect.sync(() => {\n processed.push(n)\n })\n ),\n Stream.runDrain\n )\n )\n\n expect(processed).toEqual([1, 2, 3])\n })\n\n // ============================================\n // 6. Test stream resource management\n // ============================================\n\n it(\"should release resources on completion\", async () => {\n const acquired: string[] = []\n const released: string[] = []\n\n const managedStream = Stream.acquireRelease(\n Effect.gen(function* () {\n acquired.push(\"resource\")\n return \"resource\"\n }),\n () =>\n Effect.sync(() => {\n released.push(\"resource\")\n })\n ).pipe(\n Stream.flatMap(() => Stream.fromIterable([1, 2, 3]))\n )\n\n await Effect.runPromise(Stream.runDrain(managedStream))\n\n expect(acquired).toEqual([\"resource\"])\n expect(released).toEqual([\"resource\"])\n })\n\n it(\"should release resources on error\", async () => {\n const released: string[] = []\n\n const managedStream = Stream.acquireRelease(\n Effect.succeed(\"resource\"),\n () => Effect.sync(() => { released.push(\"released\") })\n ).pipe(\n Stream.flatMap(() =>\n Stream.fromEffect(Effect.fail(new Error(\"Oops\")))\n )\n )\n\n await Effect.runPromise(\n Stream.runDrain(managedStream).pipe(\n Effect.catchAll(() => Effect.void)\n )\n )\n\n expect(released).toEqual([\"released\"])\n })\n\n // ============================================\n // 7. Test stream timing with take/drop\n // ============================================\n\n it(\"should take first N elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.take(3),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])\n })\n\n it(\"should drop first N elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.drop(2),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([3, 4, 5])\n })\n\n // ============================================\n // 8. Test stream merging\n // ============================================\n\n it(\"should merge streams\", async () => {\n const stream1 = Stream.fromIterable([1, 3, 5])\n const stream2 = Stream.fromIterable([2, 4, 6])\n\n const result = await Effect.runPromise(\n Stream.merge(stream1, stream2).pipe(\n Stream.runCollect\n )\n )\n\n const array = Chunk.toReadonlyArray(result)\n expect(array).toHaveLength(6)\n expect(array).toContain(1)\n expect(array).toContain(6)\n })\n})\n```", - "antiPattern": "", - "explanation": "Stream tests verify:\n\n1. **Transformations** - map, filter, flatMap work correctly\n2. **Error handling** - Failures are caught and handled\n3. **Resource safety** - Resources are released\n4. **Backpressure** - Data flow is controlled\n\n---", - "content": "## Guideline\n\nTest streams by collecting results and verifying transformations, error handling, and resource management.\n\n---\n\n## Rationale\n\nStream tests verify:\n\n1. **Transformations** - map, filter, flatMap work correctly\n2. **Error handling** - Failures are caught and handled\n3. **Resource safety** - Resources are released\n4. **Backpressure** - Data flow is controlled\n\n---\n\n## Good Example\n\n```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect, Stream, Chunk, Ref } from \"effect\"\n\ndescribe(\"Stream Testing\", () => {\n // ============================================\n // 1. Test basic stream operations\n // ============================================\n\n it(\"should transform stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.map((n) => n * 2),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6, 8, 10])\n })\n\n it(\"should filter stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5, 6]).pipe(\n Stream.filter((n) => n % 2 === 0),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6])\n })\n\n // ============================================\n // 2. Test stream aggregation\n // ============================================\n\n it(\"should fold stream to single value\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.runFold(0, (acc, n) => acc + n)\n )\n )\n\n expect(result).toBe(15)\n })\n\n it(\"should count stream elements\", async () => {\n const count = await Effect.runPromise(\n Stream.fromIterable([\"a\", \"b\", \"c\", \"d\"]).pipe(\n Stream.runCount\n )\n )\n\n expect(count).toBe(4)\n })\n\n // ============================================\n // 3. Test error handling in streams\n // ============================================\n\n it(\"should catch errors in stream\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n Stream.mapEffect((n) =>\n n === 2\n ? Effect.fail(new Error(\"Failed on 2\"))\n : Effect.succeed(n * 10)\n ),\n Stream.catchAll((error) =>\n Stream.succeed(-1) // Replace error with sentinel\n ),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([10, -1])\n })\n\n it(\"should handle errors and continue with orElse\", async () => {\n const failingStream = Stream.fail(new Error(\"Primary failed\"))\n const fallbackStream = Stream.fromIterable([1, 2, 3])\n\n const result = await Effect.runPromise(\n failingStream.pipe(\n Stream.orElse(() => fallbackStream),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])\n })\n\n // ============================================\n // 4. Test stream chunking\n // ============================================\n\n it(\"should chunk stream elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.grouped(2),\n Stream.runCollect\n )\n )\n\n const chunks = Chunk.toReadonlyArray(result).map(Chunk.toReadonlyArray)\n expect(chunks).toEqual([[1, 2], [3, 4], [5]])\n })\n\n // ============================================\n // 5. Test stream with effects\n // ============================================\n\n it(\"should run effects for each element\", async () => {\n const processed: number[] = []\n\n await Effect.runPromise(\n Stream.fromIterable([1, 2, 3]).pipe(\n Stream.tap((n) =>\n Effect.sync(() => {\n processed.push(n)\n })\n ),\n Stream.runDrain\n )\n )\n\n expect(processed).toEqual([1, 2, 3])\n })\n\n // ============================================\n // 6. Test stream resource management\n // ============================================\n\n it(\"should release resources on completion\", async () => {\n const acquired: string[] = []\n const released: string[] = []\n\n const managedStream = Stream.acquireRelease(\n Effect.gen(function* () {\n acquired.push(\"resource\")\n return \"resource\"\n }),\n () =>\n Effect.sync(() => {\n released.push(\"resource\")\n })\n ).pipe(\n Stream.flatMap(() => Stream.fromIterable([1, 2, 3]))\n )\n\n await Effect.runPromise(Stream.runDrain(managedStream))\n\n expect(acquired).toEqual([\"resource\"])\n expect(released).toEqual([\"resource\"])\n })\n\n it(\"should release resources on error\", async () => {\n const released: string[] = []\n\n const managedStream = Stream.acquireRelease(\n Effect.succeed(\"resource\"),\n () => Effect.sync(() => { released.push(\"released\") })\n ).pipe(\n Stream.flatMap(() =>\n Stream.fromEffect(Effect.fail(new Error(\"Oops\")))\n )\n )\n\n await Effect.runPromise(\n Stream.runDrain(managedStream).pipe(\n Effect.catchAll(() => Effect.void)\n )\n )\n\n expect(released).toEqual([\"released\"])\n })\n\n // ============================================\n // 7. Test stream timing with take/drop\n // ============================================\n\n it(\"should take first N elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.take(3),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])\n })\n\n it(\"should drop first N elements\", async () => {\n const result = await Effect.runPromise(\n Stream.fromIterable([1, 2, 3, 4, 5]).pipe(\n Stream.drop(2),\n Stream.runCollect\n )\n )\n\n expect(Chunk.toReadonlyArray(result)).toEqual([3, 4, 5])\n })\n\n // ============================================\n // 8. Test stream merging\n // ============================================\n\n it(\"should merge streams\", async () => {\n const stream1 = Stream.fromIterable([1, 3, 5])\n const stream2 = Stream.fromIterable([2, 4, 6])\n\n const result = await Effect.runPromise(\n Stream.merge(stream1, stream2).pipe(\n Stream.runCollect\n )\n )\n\n const array = Chunk.toReadonlyArray(result)\n expect(array).toHaveLength(6)\n expect(array).toContain(1)\n expect(array).toContain(6)\n })\n})\n```\n\n## Test Patterns\n\n| Pattern | How to Test |\n|---------|-------------|\n| Transformations | runCollect + compare array |\n| Aggregation | runFold, runCount |\n| Errors | catchAll + verify recovery |\n| Resources | Track acquire/release calls |\n| Side effects | Use tap + external tracking |\n\n## Key Assertions\n\n| Function | Returns |\n|----------|---------|\n| `Stream.runCollect` | Chunk of all elements |\n| `Stream.runFold` | Single aggregated value |\n| `Stream.runCount` | Number of elements |\n| `Stream.runDrain` | void (side effects) |\n| `Stream.runHead` | First element (Option) |" - }, - { - "id": "observability-tracing-spans", - "title": "Trace Operations Across Services with Spans", - "description": "Use Effect.withSpan to create and annotate tracing spans for operations, enabling distributed tracing and performance analysis.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Trace a database query with a custom span\nconst fetchUser = Effect.sync(() => {\n // ...fetch user from database\n return { id: 1, name: \"Alice\" };\n}).pipe(Effect.withSpan(\"db.fetchUser\"));\n\n// Trace an HTTP request with additional attributes\nconst fetchData = Effect.tryPromise({\n try: () => fetch(\"https://api.example.com/data\").then((res) => res.json()),\n catch: (err) => `Network error: ${String(err)}`,\n}).pipe(\n Effect.withSpan(\"http.fetchData\", {\n attributes: { url: \"https://api.example.com/data\" },\n })\n);\n\n// Use spans in a workflow\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting workflow\").pipe(\n Effect.withSpan(\"workflow.start\")\n );\n const user = yield* fetchUser;\n yield* Effect.log(`Fetched user: ${user.name}`).pipe(\n Effect.withSpan(\"workflow.end\")\n );\n});\n```\n\n**Explanation:**\n\n- `Effect.withSpan` creates a tracing span around an operation.\n- Spans can be named and annotated with attributes for richer context.\n- Tracing enables distributed observability and performance analysis.", - "antiPattern": "Relying only on logs or metrics for performance analysis, or lacking visibility into the flow of requests and operations across services.", - "explanation": "Tracing spans help you understand the flow and timing of operations, especially in distributed systems or complex workflows. \nThey allow you to pinpoint bottlenecks, visualize dependencies, and correlate logs and metrics with specific requests.", - "content": "# Trace Operations Across Services with Spans\n\n## Guideline\n\nUse `Effect.withSpan` to create custom tracing spans around important operations in your application. \nThis enables distributed tracing, performance analysis, and deep visibility into how requests flow through your system.\n\n## Rationale\n\nTracing spans help you understand the flow and timing of operations, especially in distributed systems or complex workflows. \nThey allow you to pinpoint bottlenecks, visualize dependencies, and correlate logs and metrics with specific requests.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Trace a database query with a custom span\nconst fetchUser = Effect.sync(() => {\n // ...fetch user from database\n return { id: 1, name: \"Alice\" };\n}).pipe(Effect.withSpan(\"db.fetchUser\"));\n\n// Trace an HTTP request with additional attributes\nconst fetchData = Effect.tryPromise({\n try: () => fetch(\"https://api.example.com/data\").then((res) => res.json()),\n catch: (err) => `Network error: ${String(err)}`,\n}).pipe(\n Effect.withSpan(\"http.fetchData\", {\n attributes: { url: \"https://api.example.com/data\" },\n })\n);\n\n// Use spans in a workflow\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Starting workflow\").pipe(\n Effect.withSpan(\"workflow.start\")\n );\n const user = yield* fetchUser;\n yield* Effect.log(`Fetched user: ${user.name}`).pipe(\n Effect.withSpan(\"workflow.end\")\n );\n});\n```\n\n**Explanation:**\n\n- `Effect.withSpan` creates a tracing span around an operation.\n- Spans can be named and annotated with attributes for richer context.\n- Tracing enables distributed observability and performance analysis.\n\n## Anti-Pattern\n\nRelying only on logs or metrics for performance analysis, or lacking visibility into the flow of requests and operations across services." - }, - { - "id": "trace-operations-with-spans", - "title": "Trace Operations Across Services with Spans", - "description": "Use Effect.withSpan to create custom tracing spans for important operations.", - "skillLevel": "intermediate", - "useCases": [ - "observability" - ], - "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.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\n\nconst validateInput = (input: unknown) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting input validation...\");\n yield* Effect.sleep(Duration.millis(10));\n const result = { email: \"paul@example.com\" };\n yield* Effect.logInfo(`✅ Input validated: ${result.email}`);\n return result;\n }).pipe(\n // This creates a child span\n Effect.withSpan(\"validateInput\")\n );\n\nconst saveToDatabase = (user: { email: string }) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Saving user to database: ${user.email}`);\n yield* Effect.sleep(Duration.millis(50));\n const result = { id: 123, ...user };\n yield* Effect.logInfo(`✅ User saved with ID: ${result.id}`);\n return result;\n }).pipe(\n // This span includes useful attributes\n Effect.withSpan(\"saveToDatabase\", {\n attributes: { \"db.system\": \"postgresql\", \"db.user.email\": user.email },\n })\n );\n\nconst createUser = (input: unknown) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Creating User with Tracing ===\");\n yield* Effect.logInfo(\n \"This demonstrates how spans trace operations through the call stack\"\n );\n\n const validated = yield* validateInput(input);\n const user = yield* saveToDatabase(validated);\n\n yield* Effect.logInfo(\n `✅ User creation completed: ${JSON.stringify(user)}`\n );\n yield* Effect.logInfo(\n \"Note: In production, spans would be sent to a tracing system like Jaeger or Zipkin\"\n );\n\n return user;\n }).pipe(\n // This is the parent span for the entire operation\n Effect.withSpan(\"createUserOperation\")\n );\n\n// Demonstrate the tracing functionality\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Trace Operations with Spans Demo ===\");\n\n // Create multiple users to show tracing in action\n const user1 = yield* createUser({ email: \"user1@example.com\" });\n\n yield* Effect.logInfo(\"\\n--- Creating second user ---\");\n const user2 = yield* createUser({ email: \"user2@example.com\" });\n\n yield* Effect.logInfo(\"\\n=== Summary ===\");\n yield* Effect.logInfo(\"Created users with tracing spans:\");\n yield* Effect.logInfo(`User 1: ID ${user1.id}, Email: ${user1.email}`);\n yield* Effect.logInfo(`User 2: ID ${user2.id}, Email: ${user2.email}`);\n});\n\n// When run with a tracing SDK, this will produce traces with root spans\n// \"createUserOperation\" and child spans: \"validateInput\" and \"saveToDatabase\".\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "Not adding custom spans to your business logic.\nWithout them, your traces will only show high-level information from your framework (e.g., \"HTTP POST /users\").\nYou will have no visibility into the performance of the individual steps _inside_ your request handler, making it very difficult to pinpoint bottlenecks. Your application's logic remains a \"black box\" in your traces.", - "explanation": "While logs tell you _what_ happened, traces tell you _why it was slow_. In a complex application, a single user request might trigger calls to multiple services (authentication, database, external APIs). Tracing allows you to visualize this entire chain of events as a single, hierarchical \"trace.\"\n\nEach piece of work in that trace is a `span`. `Effect.withSpan` allows you to create your own custom spans. This is invaluable for answering questions like:\n\n- \"For this API request, did we spend most of our time in the database or calling the external payment gateway?\"\n- \"Which part of our user creation logic is the bottleneck?\"\n\nEffect's tracing is built on OpenTelemetry, the industry standard, so it integrates seamlessly with tools like Jaeger, Zipkin, and Datadog.\n\n---", - "content": "## Guideline\n\nTo gain visibility into the performance and flow of your application, wrap logical units of work with `Effect.withSpan(\"span-name\")`. You can add contextual information to these spans using the `attributes` option.\n\n---\n\n## Rationale\n\nWhile logs tell you _what_ happened, traces tell you _why it was slow_. In a complex application, a single user request might trigger calls to multiple services (authentication, database, external APIs). Tracing allows you to visualize this entire chain of events as a single, hierarchical \"trace.\"\n\nEach piece of work in that trace is a `span`. `Effect.withSpan` allows you to create your own custom spans. This is invaluable for answering questions like:\n\n- \"For this API request, did we spend most of our time in the database or calling the external payment gateway?\"\n- \"Which part of our user creation logic is the bottleneck?\"\n\nEffect's tracing is built on OpenTelemetry, the industry standard, so it integrates seamlessly with tools like Jaeger, Zipkin, and Datadog.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Duration } from \"effect\";\n\nconst validateInput = (input: unknown) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Starting input validation...\");\n yield* Effect.sleep(Duration.millis(10));\n const result = { email: \"paul@example.com\" };\n yield* Effect.logInfo(`✅ Input validated: ${result.email}`);\n return result;\n }).pipe(\n // This creates a child span\n Effect.withSpan(\"validateInput\")\n );\n\nconst saveToDatabase = (user: { email: string }) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Saving user to database: ${user.email}`);\n yield* Effect.sleep(Duration.millis(50));\n const result = { id: 123, ...user };\n yield* Effect.logInfo(`✅ User saved with ID: ${result.id}`);\n return result;\n }).pipe(\n // This span includes useful attributes\n Effect.withSpan(\"saveToDatabase\", {\n attributes: { \"db.system\": \"postgresql\", \"db.user.email\": user.email },\n })\n );\n\nconst createUser = (input: unknown) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Creating User with Tracing ===\");\n yield* Effect.logInfo(\n \"This demonstrates how spans trace operations through the call stack\"\n );\n\n const validated = yield* validateInput(input);\n const user = yield* saveToDatabase(validated);\n\n yield* Effect.logInfo(\n `✅ User creation completed: ${JSON.stringify(user)}`\n );\n yield* Effect.logInfo(\n \"Note: In production, spans would be sent to a tracing system like Jaeger or Zipkin\"\n );\n\n return user;\n }).pipe(\n // This is the parent span for the entire operation\n Effect.withSpan(\"createUserOperation\")\n );\n\n// Demonstrate the tracing functionality\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Trace Operations with Spans Demo ===\");\n\n // Create multiple users to show tracing in action\n const user1 = yield* createUser({ email: \"user1@example.com\" });\n\n yield* Effect.logInfo(\"\\n--- Creating second user ---\");\n const user2 = yield* createUser({ email: \"user2@example.com\" });\n\n yield* Effect.logInfo(\"\\n=== Summary ===\");\n yield* Effect.logInfo(\"Created users with tracing spans:\");\n yield* Effect.logInfo(`User 1: ID ${user1.id}, Email: ${user1.email}`);\n yield* Effect.logInfo(`User 2: ID ${user2.id}, Email: ${user2.email}`);\n});\n\n// When run with a tracing SDK, this will produce traces with root spans\n// \"createUserOperation\" and child spans: \"validateInput\" and \"saveToDatabase\".\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern\n\nNot adding custom spans to your business logic.\nWithout them, your traces will only show high-level information from your framework (e.g., \"HTTP POST /users\").\nYou will have no visibility into the performance of the individual steps _inside_ your request handler, making it very difficult to pinpoint bottlenecks. Your application's logic remains a \"black box\" in your traces." - }, - { - "id": "transform-data-with-schema", - "title": "Transform Data During Validation with Schema", - "description": "Use Schema.transform to safely convert data types during the validation and parsing process.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "This schema parses a string but produces a `Date` object, making the final data structure much more useful.\n\n```typescript\nimport { Schema, Effect } from \"effect\";\n\n// Define types for better type safety\ntype RawEvent = {\n name: string;\n timestamp: string;\n};\n\ntype ParsedEvent = {\n name: string;\n timestamp: Date;\n};\n\n// Define the schema for our event\nconst ApiEventSchema = Schema.Struct({\n name: Schema.String,\n timestamp: Schema.String,\n});\n\n// Example input\nconst rawInput: RawEvent = {\n name: \"User Login\",\n timestamp: \"2025-06-22T20:08:42.000Z\",\n};\n\n// Parse and transform\nconst program = Effect.gen(function* () {\n const parsed = yield* Schema.decode(ApiEventSchema)(rawInput);\n return {\n name: parsed.name,\n timestamp: new Date(parsed.timestamp),\n } as ParsedEvent;\n});\n\nconst programWithLogging = Effect.gen(function* () {\n try {\n const event = yield* program;\n yield* Effect.log(`Event year: ${event.timestamp.getFullYear()}`);\n yield* Effect.log(`Full event: ${JSON.stringify(event, null, 2)}`);\n return event;\n } catch (error) {\n yield* Effect.logError(`Failed to parse event: ${error}`);\n throw error;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n```\n\n\n`transformOrFail` is perfect for creating branded types, as the validation can fail.\n\n```typescript\nimport { Schema, Effect, Brand, Either } from \"effect\";\n\ntype Email = string & Brand.Brand<\"Email\">;\nconst Email = Schema.string.pipe(\n Schema.transformOrFail(\n Schema.brand(\"Email\"),\n (s, _, ast) =>\n s.includes(\"@\")\n ? Either.right(s as Email)\n : Either.left(Schema.ParseError.create(ast, \"Invalid email format\")),\n (email) => Either.right(email)\n )\n);\n\nconst result = Schema.decode(Email)(\"paul@example.com\"); // Succeeds\nconst errorResult = Schema.decode(Email)(\"invalid-email\"); // Fails\n```\n\n---", - "antiPattern": "Performing validation and transformation in two separate steps. This is more verbose, requires creating intermediate types, and separates the validation logic from the transformation logic.\n\n```typescript\nimport { Schema, Effect } from \"effect\";\n\n// ❌ WRONG: Requires an intermediate \"Raw\" type.\nconst RawApiEventSchema = Schema.Struct({\n name: Schema.String,\n timestamp: Schema.String,\n});\n\nconst rawInput = { name: \"User Login\", timestamp: \"2025-06-22T20:08:42.000Z\" };\n\n// The logic is now split into two distinct, less cohesive steps.\nconst program = Schema.decode(RawApiEventSchema)(rawInput).pipe(\n Effect.map((rawEvent) => ({\n ...rawEvent,\n timestamp: new Date(rawEvent.timestamp), // Manual transformation after parsing.\n }))\n);\n```", - "explanation": "Often, the data you receive from external sources (like an API) isn't in the ideal format for your application's domain model. For example, dates are sent as ISO strings, but you want to work with `Date` objects.\n\n`Schema.transform` integrates this conversion directly into the parsing step. It takes two functions: one to `decode` the input type into the domain type, and one to `encode` it back. This makes your schema the single source of truth for both the shape and the type transformation of your data.\n\nFor transformations that can fail (like creating a branded type), you can use `Schema.transformOrFail`, which allows the decoding step to return an `Either`.\n\n---", - "content": "## Guideline\n\nTo convert data from one type to another as part of the validation process, use `Schema.transform`. This allows you to define a schema that parses an input type (e.g., `string`) and outputs a different, richer domain type (e.g., `Date`).\n\n---\n\n## Rationale\n\nOften, the data you receive from external sources (like an API) isn't in the ideal format for your application's domain model. For example, dates are sent as ISO strings, but you want to work with `Date` objects.\n\n`Schema.transform` integrates this conversion directly into the parsing step. It takes two functions: one to `decode` the input type into the domain type, and one to `encode` it back. This makes your schema the single source of truth for both the shape and the type transformation of your data.\n\nFor transformations that can fail (like creating a branded type), you can use `Schema.transformOrFail`, which allows the decoding step to return an `Either`.\n\n---\n\n## Good Example 1: Parsing a Date String\n\nThis schema parses a string but produces a `Date` object, making the final data structure much more useful.\n\n```typescript\nimport { Schema, Effect } from \"effect\";\n\n// Define types for better type safety\ntype RawEvent = {\n name: string;\n timestamp: string;\n};\n\ntype ParsedEvent = {\n name: string;\n timestamp: Date;\n};\n\n// Define the schema for our event\nconst ApiEventSchema = Schema.Struct({\n name: Schema.String,\n timestamp: Schema.String,\n});\n\n// Example input\nconst rawInput: RawEvent = {\n name: \"User Login\",\n timestamp: \"2025-06-22T20:08:42.000Z\",\n};\n\n// Parse and transform\nconst program = Effect.gen(function* () {\n const parsed = yield* Schema.decode(ApiEventSchema)(rawInput);\n return {\n name: parsed.name,\n timestamp: new Date(parsed.timestamp),\n } as ParsedEvent;\n});\n\nconst programWithLogging = Effect.gen(function* () {\n try {\n const event = yield* program;\n yield* Effect.log(`Event year: ${event.timestamp.getFullYear()}`);\n yield* Effect.log(`Full event: ${JSON.stringify(event, null, 2)}`);\n return event;\n } catch (error) {\n yield* Effect.logError(`Failed to parse event: ${error}`);\n throw error;\n }\n}).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Program error: ${error}`);\n return null;\n })\n )\n);\n\nEffect.runPromise(programWithLogging);\n```\n\n## Good Example 2: Creating a Branded Type\n\n`transformOrFail` is perfect for creating branded types, as the validation can fail.\n\n```typescript\nimport { Schema, Effect, Brand, Either } from \"effect\";\n\ntype Email = string & Brand.Brand<\"Email\">;\nconst Email = Schema.string.pipe(\n Schema.transformOrFail(\n Schema.brand(\"Email\"),\n (s, _, ast) =>\n s.includes(\"@\")\n ? Either.right(s as Email)\n : Either.left(Schema.ParseError.create(ast, \"Invalid email format\")),\n (email) => Either.right(email)\n )\n);\n\nconst result = Schema.decode(Email)(\"paul@example.com\"); // Succeeds\nconst errorResult = Schema.decode(Email)(\"invalid-email\"); // Fails\n```\n\n---\n\n## Anti-Pattern\n\nPerforming validation and transformation in two separate steps. This is more verbose, requires creating intermediate types, and separates the validation logic from the transformation logic.\n\n```typescript\nimport { Schema, Effect } from \"effect\";\n\n// ❌ WRONG: Requires an intermediate \"Raw\" type.\nconst RawApiEventSchema = Schema.Struct({\n name: Schema.String,\n timestamp: Schema.String,\n});\n\nconst rawInput = { name: \"User Login\", timestamp: \"2025-06-22T20:08:42.000Z\" };\n\n// The logic is now split into two distinct, less cohesive steps.\nconst program = Schema.decode(RawApiEventSchema)(rawInput).pipe(\n Effect.map((rawEvent) => ({\n ...rawEvent,\n timestamp: new Date(rawEvent.timestamp), // Manual transformation after parsing.\n }))\n);\n```" - }, - { - "id": "transform-effect-values", - "title": "Transform Effect Values with map and flatMap", - "description": "Transform Effect values with map and flatMap.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst getUser = (id: number): Effect.Effect<{ id: number; name: string }> =>\n Effect.succeed({ id, name: \"Paul\" });\n\nconst getPosts = (userId: number): Effect.Effect<{ title: string }[]> =>\n Effect.succeed([{ title: \"My First Post\" }, { title: \"Second Post\" }]);\n\nconst userPosts = getUser(123).pipe(\n Effect.flatMap((user) => getPosts(user.id))\n);\n\n// Demonstrate transforming Effect values\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Transform Effect Values Demo ===\");\n\n // 1. Basic transformation with map\n yield* Effect.log(\"\\n1. Transform with map:\");\n const userWithUpperName = yield* getUser(123).pipe(\n Effect.map((user) => ({ ...user, name: user.name.toUpperCase() }))\n );\n yield* Effect.log(\"Transformed user:\", userWithUpperName);\n\n // 2. Chain effects with flatMap\n yield* Effect.log(\"\\n2. Chain effects with flatMap:\");\n const posts = yield* userPosts;\n yield* Effect.log(\"User posts:\", posts);\n\n // 3. Transform and combine multiple effects\n yield* Effect.log(\"\\n3. Transform and combine multiple effects:\");\n const userWithPosts = yield* getUser(456).pipe(\n Effect.flatMap((user) =>\n getPosts(user.id).pipe(\n Effect.map((posts) => ({\n user: user.name,\n postCount: posts.length,\n titles: posts.map((p) => p.title),\n }))\n )\n )\n );\n yield* Effect.log(\"User with posts:\", userWithPosts);\n\n // 4. Transform with tap for side effects\n yield* Effect.log(\"\\n4. Transform with tap for side effects:\");\n const result = yield* getUser(789).pipe(\n Effect.tap((user) => Effect.log(`Processing user: ${user.name}`)),\n Effect.map((user) => `Hello, ${user.name}!`)\n );\n yield* Effect.log(\"Final result:\", result);\n\n yield* Effect.log(\"\\n✅ All transformations completed successfully!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `flatMap` to chain effects that depend on each other, and `map` for\nsimple value transformations.", - "antiPattern": "Using `map` when you should be using `flatMap`. This results in a nested\n`Effect>`, which is usually not what you want.", - "explanation": "`Effect.map` is like `Array.prototype.map`. `Effect.flatMap` is like\n`Promise.prototype.then` and is used when your transformation function itself\nreturns an `Effect`.", - "content": "# Transform Effect Values with map and flatMap\n\n## Guideline\n\nTo work with the success value of an `Effect`, use `Effect.map` for simple,\nsynchronous transformations and `Effect.flatMap` for effectful transformations.\n\n## Rationale\n\n`Effect.map` is like `Array.prototype.map`. `Effect.flatMap` is like\n`Promise.prototype.then` and is used when your transformation function itself\nreturns an `Effect`.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst getUser = (id: number): Effect.Effect<{ id: number; name: string }> =>\n Effect.succeed({ id, name: \"Paul\" });\n\nconst getPosts = (userId: number): Effect.Effect<{ title: string }[]> =>\n Effect.succeed([{ title: \"My First Post\" }, { title: \"Second Post\" }]);\n\nconst userPosts = getUser(123).pipe(\n Effect.flatMap((user) => getPosts(user.id))\n);\n\n// Demonstrate transforming Effect values\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Transform Effect Values Demo ===\");\n\n // 1. Basic transformation with map\n yield* Effect.log(\"\\n1. Transform with map:\");\n const userWithUpperName = yield* getUser(123).pipe(\n Effect.map((user) => ({ ...user, name: user.name.toUpperCase() }))\n );\n yield* Effect.log(\"Transformed user:\", userWithUpperName);\n\n // 2. Chain effects with flatMap\n yield* Effect.log(\"\\n2. Chain effects with flatMap:\");\n const posts = yield* userPosts;\n yield* Effect.log(\"User posts:\", posts);\n\n // 3. Transform and combine multiple effects\n yield* Effect.log(\"\\n3. Transform and combine multiple effects:\");\n const userWithPosts = yield* getUser(456).pipe(\n Effect.flatMap((user) =>\n getPosts(user.id).pipe(\n Effect.map((posts) => ({\n user: user.name,\n postCount: posts.length,\n titles: posts.map((p) => p.title),\n }))\n )\n )\n );\n yield* Effect.log(\"User with posts:\", userWithPosts);\n\n // 4. Transform with tap for side effects\n yield* Effect.log(\"\\n4. Transform with tap for side effects:\");\n const result = yield* getUser(789).pipe(\n Effect.tap((user) => Effect.log(`Processing user: ${user.name}`)),\n Effect.map((user) => `Hello, ${user.name}!`)\n );\n yield* Effect.log(\"Final result:\", result);\n\n yield* Effect.log(\"\\n✅ All transformations completed successfully!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `flatMap` to chain effects that depend on each other, and `map` for\nsimple value transformations.\n\n## Anti-Pattern\n\nUsing `map` when you should be using `flatMap`. This results in a nested\n`Effect>`, which is usually not what you want." - }, - { - "id": "getting-started-transform-with-map", - "title": "Transform Values with Effect.map", - "description": "Transform Effect values with map.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Start with an Effect that succeeds with a number\nconst getNumber = Effect.succeed(5);\n\n// Transform it: multiply by 2\nconst doubled = Effect.map(getNumber, (n) => n * 2);\n\n// Transform again: convert to string\nconst asString = Effect.map(doubled, (n) => `The result is ${n}`);\n\n// Run to see the result\nconst result = Effect.runSync(asString);\nconsole.log(result); // \"The result is 10\"\n```", - "antiPattern": "", - "explanation": "Just like `Array.map` transforms array elements, `Effect.map` transforms\nthe success value of an Effect. This lets you build pipelines of\ntransformations without running anything until the end.", - "content": "# Transform Values with Effect.map\n\n## Guideline\n\nUse `Effect.map` to transform the success value inside an Effect. The\ntransformation function receives the value and returns a new value.\n\n## Rationale\n\nJust like `Array.map` transforms array elements, `Effect.map` transforms\nthe success value of an Effect. This lets you build pipelines of\ntransformations without running anything until the end.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Start with an Effect that succeeds with a number\nconst getNumber = Effect.succeed(5);\n\n// Transform it: multiply by 2\nconst doubled = Effect.map(getNumber, (n) => n * 2);\n\n// Transform again: convert to string\nconst asString = Effect.map(doubled, (n) => `The result is ${n}`);\n\n// Run to see the result\nconst result = Effect.runSync(asString);\nconsole.log(result); // \"The result is 10\"\n```\n\n## Using pipe for Cleaner Code\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\nconst result = pipe(\n Effect.succeed(5),\n Effect.map((n) => n * 2),\n Effect.map((n) => n + 1),\n Effect.map((n) => `Final value: ${n}`),\n Effect.runSync\n);\n\nconsole.log(result); // \"Final value: 11\"\n```\n\n## Real-World Example\n\n```typescript\nimport { Effect, pipe } from \"effect\";\n\ninterface User {\n id: number;\n name: string;\n email: string;\n}\n\nconst fetchUser = Effect.succeed({\n id: 1,\n name: \"Alice\",\n email: \"alice@example.com\",\n});\n\n// Extract just the name from the user\nconst getUserName = pipe(\n fetchUser,\n Effect.map((user) => user.name)\n);\n\n// Format the name for display\nconst getDisplayName = pipe(\n fetchUser,\n Effect.map((user) => user.name.toUpperCase())\n);\n\nEffect.runSync(Effect.all([getUserName, getDisplayName]));\n// [\"Alice\", \"ALICE\"]\n```\n\n## Key Points\n\n1. **Effect.map** only transforms success values - errors pass through unchanged\n2. **The original Effect is unchanged** - map creates a new Effect\n3. **Nothing runs until you call runSync/runPromise** - you're just building a pipeline\n\n## What's Next?\n\n- Learn `Effect.flatMap` for chaining Effects that return other Effects\n- Learn error handling with `Effect.catchAll`" - }, - { - "id": "combinator-map", - "title": "Transforming Values with map", - "description": "Use map to apply a pure function to the value inside an Effect, Stream, Option, or Either.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Transform the result of an effect\nconst effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect\n\n// Option: Transform an optional value\nconst option = Option.some(2).pipe(Option.map((n) => n * 10)); // Option\n\n// Either: Transform a value that may be an error\nconst either = Either.right(2).pipe(Either.map((n) => n * 10)); // Either\n\n// Stream: Transform every value in a stream\nconst stream = Stream.fromIterable([1, 2, 3]).pipe(Stream.map((n) => n * 10)); // Stream\n```\n\n**Explanation:** \nNo matter which type you use, `map` lets you apply a function to the value inside, without changing the error or context.", - "antiPattern": "Manually extracting the value (e.g., with `.getOrElse`, `.unsafeRunSync`, or similar) just to transform it, then re-wrapping it. \nThis breaks composability and loses the benefits of type safety and error handling.", - "explanation": "`map` is the most fundamental combinator in functional programming. \nIt allows you to focus on _what_ you want to do with a value, not _how_ to extract it. \nThe same mental model applies across all major Effect types.", - "content": "# Transforming Values with `map`\n\n## Guideline\n\nUse the `map` combinator to apply a pure function to the value inside an `Effect`, `Stream`, `Option`, or `Either`. \nThis lets you transform results without changing the structure or error-handling behavior of the original type.\n\n## Rationale\n\n`map` is the most fundamental combinator in functional programming. \nIt allows you to focus on _what_ you want to do with a value, not _how_ to extract it. \nThe same mental model applies across all major Effect types.\n\n## Good Example\n\n```typescript\nimport { Effect, Stream, Option, Either } from \"effect\";\n\n// Effect: Transform the result of an effect\nconst effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect\n\n// Option: Transform an optional value\nconst option = Option.some(2).pipe(Option.map((n) => n * 10)); // Option\n\n// Either: Transform a value that may be an error\nconst either = Either.right(2).pipe(Either.map((n) => n * 10)); // Either\n\n// Stream: Transform every value in a stream\nconst stream = Stream.fromIterable([1, 2, 3]).pipe(Stream.map((n) => n * 10)); // Stream\n```\n\n**Explanation:** \nNo matter which type you use, `map` lets you apply a function to the value inside, without changing the error or context.\n\n## Anti-Pattern\n\nManually extracting the value (e.g., with `.getOrElse`, `.unsafeRunSync`, or similar) just to transform it, then re-wrapping it. \nThis breaks composability and loses the benefits of type safety and error handling." - }, - { - "id": "stream-from-paginated-api", - "title": "Turn a Paginated API into a Single Stream", - "description": "Use Stream.paginateEffect to model a paginated data source as a single, continuous stream.", - "skillLevel": "intermediate", - "useCases": [ - "building-data-pipelines" - ], - "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.\n\n```typescript\nimport { Effect, Stream, Chunk, Option } from \"effect\";\n\n// --- Mock Paginated API ---\ninterface User {\n id: number;\n name: string;\n}\n\n// Define FetchError as a class with a literal type tag\nclass FetchError {\n readonly _tag = \"FetchError\" as const;\n constructor(readonly message: string) {}\n}\n\n// Helper to create FetchError instances\nconst fetchError = (message: string): FetchError => new FetchError(message);\n\nconst allUsers: User[] = Array.from({ length: 25 }, (_, i) => ({\n id: i + 1,\n name: `User ${i + 1}`,\n}));\n\n// This function simulates fetching a page of users from an API.\nconst fetchUsersPage = (\n page: number\n): Effect.Effect<[Chunk.Chunk, Option.Option], FetchError> =>\n Effect.gen(function* () {\n const pageSize = 10;\n const offset = (page - 1) * pageSize;\n\n // Simulate potential API errors\n if (page < 1) {\n return yield* Effect.fail(fetchError(\"Invalid page number\"));\n }\n\n const users = Chunk.fromIterable(allUsers.slice(offset, offset + pageSize));\n\n const nextPage =\n Chunk.isNonEmpty(users) && allUsers.length > offset + pageSize\n ? Option.some(page + 1)\n : Option.none();\n\n yield* Effect.log(`Fetched page ${page}`);\n return [users, nextPage];\n });\n\n// --- The Pattern ---\n// Use paginateEffect, providing an initial state (page 1) and the fetch function.\nconst userStream = Stream.paginateEffect(1, fetchUsersPage);\n\nconst program = userStream.pipe(\n Stream.runCollect,\n Effect.map((users) => users.length),\n Effect.tap((totalUsers) => Effect.log(`Total users fetched: ${totalUsers}`)),\n Effect.catchTag(\"FetchError\", (error) =>\n Effect.succeed(`Error fetching users: ${error.message}`)\n )\n);\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Program result: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n\n/*\nOutput:\n... level=INFO msg=\"Fetched page 1\"\n... level=INFO msg=\"Fetched page 2\"\n... level=INFO msg=\"Fetched page 3\"\n... level=INFO msg=\"Total users fetched: 25\"\n25\n*/\n```", - "antiPattern": "The anti-pattern is to write manual, imperative logic to handle the pagination loop. This code is stateful, harder to read, and not composable.\n\n```typescript\nimport { Effect, Chunk, Option } from \"effect\";\n// ... same mock API setup ...\n\nconst fetchAllUsersManually = (): Effect.Effect, Error> =>\n Effect.gen(function* () {\n // Manual state management for results and current page\n let allFetchedUsers: User[] = [];\n let currentPage: Option.Option = Option.some(1);\n\n // Manual loop to fetch pages\n while (Option.isSome(currentPage)) {\n const [users, nextPage] = yield* fetchUsersPage(currentPage.value);\n allFetchedUsers = allFetchedUsers.concat(Chunk.toArray(users));\n currentPage = nextPage;\n }\n\n return Chunk.fromIterable(allFetchedUsers);\n });\n\nconst program = fetchAllUsersManually().pipe(\n Effect.map((users) => users.length)\n);\n\nEffect.runPromise(program).then((totalUsers) => {\n console.log(`Total users fetched from all pages: ${totalUsers}`);\n});\n```\n\nThis manual approach is inferior because it forces you to manage state explicitly (`allFetchedUsers`, `currentPage`). The logic is contained within a single, monolithic effect that is not lazy and cannot be easily composed with other stream operators without first collecting all results. `Stream.paginateEffect` abstracts away this entire block of boilerplate code.", - "explanation": "Calling paginated APIs is a classic programming challenge. It often involves writing complex, stateful, and imperative code with manual loops to fetch one page, check if there's a next page, fetch that page, and so on, all while accumulating the results. This logic is tedious to write and easy to get wrong.\n\n`Stream.paginateEffect` elegantly solves this by declaratively modeling the pagination process:\n\n1. **Declarative and Stateless**: You provide a function that knows how to fetch a single page, and the `Stream` handles the looping, state management (the current page token/number), and termination logic for you. Your business logic remains clean and stateless.\n2. **Lazy and Efficient**: The stream fetches pages on demand as they are consumed. If a downstream consumer only needs the first 20 items, the stream will only make enough API calls to satisfy that need, rather than wastefully fetching all pages upfront.\n3. **Fully Composable**: The result is a standard `Stream`. This means you can pipe the continuous flow of items directly into other powerful operators like `mapEffect` for concurrent processing or `grouped` for batching, without ever thinking about page boundaries again.\n\n---", - "content": "## Guideline\n\nTo handle a data source that is split across multiple pages, use `Stream.paginateEffect` to abstract the pagination logic into a single, continuous `Stream`.\n\n---\n\n## Rationale\n\nCalling paginated APIs is a classic programming challenge. It often involves writing complex, stateful, and imperative code with manual loops to fetch one page, check if there's a next page, fetch that page, and so on, all while accumulating the results. This logic is tedious to write and easy to get wrong.\n\n`Stream.paginateEffect` elegantly solves this by declaratively modeling the pagination process:\n\n1. **Declarative and Stateless**: You provide a function that knows how to fetch a single page, and the `Stream` handles the looping, state management (the current page token/number), and termination logic for you. Your business logic remains clean and stateless.\n2. **Lazy and Efficient**: The stream fetches pages on demand as they are consumed. If a downstream consumer only needs the first 20 items, the stream will only make enough API calls to satisfy that need, rather than wastefully fetching all pages upfront.\n3. **Fully Composable**: The result is a standard `Stream`. This means you can pipe the continuous flow of items directly into other powerful operators like `mapEffect` for concurrent processing or `grouped` for batching, without ever thinking about page boundaries again.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Effect, Stream, Chunk, Option } from \"effect\";\n\n// --- Mock Paginated API ---\ninterface User {\n id: number;\n name: string;\n}\n\n// Define FetchError as a class with a literal type tag\nclass FetchError {\n readonly _tag = \"FetchError\" as const;\n constructor(readonly message: string) {}\n}\n\n// Helper to create FetchError instances\nconst fetchError = (message: string): FetchError => new FetchError(message);\n\nconst allUsers: User[] = Array.from({ length: 25 }, (_, i) => ({\n id: i + 1,\n name: `User ${i + 1}`,\n}));\n\n// This function simulates fetching a page of users from an API.\nconst fetchUsersPage = (\n page: number\n): Effect.Effect<[Chunk.Chunk, Option.Option], FetchError> =>\n Effect.gen(function* () {\n const pageSize = 10;\n const offset = (page - 1) * pageSize;\n\n // Simulate potential API errors\n if (page < 1) {\n return yield* Effect.fail(fetchError(\"Invalid page number\"));\n }\n\n const users = Chunk.fromIterable(allUsers.slice(offset, offset + pageSize));\n\n const nextPage =\n Chunk.isNonEmpty(users) && allUsers.length > offset + pageSize\n ? Option.some(page + 1)\n : Option.none();\n\n yield* Effect.log(`Fetched page ${page}`);\n return [users, nextPage];\n });\n\n// --- The Pattern ---\n// Use paginateEffect, providing an initial state (page 1) and the fetch function.\nconst userStream = Stream.paginateEffect(1, fetchUsersPage);\n\nconst program = userStream.pipe(\n Stream.runCollect,\n Effect.map((users) => users.length),\n Effect.tap((totalUsers) => Effect.log(`Total users fetched: ${totalUsers}`)),\n Effect.catchTag(\"FetchError\", (error) =>\n Effect.succeed(`Error fetching users: ${error.message}`)\n )\n);\n\n// Run the program\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* program;\n yield* Effect.log(`Program result: ${result}`);\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n\n/*\nOutput:\n... level=INFO msg=\"Fetched page 1\"\n... level=INFO msg=\"Fetched page 2\"\n... level=INFO msg=\"Fetched page 3\"\n... level=INFO msg=\"Total users fetched: 25\"\n25\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to write manual, imperative logic to handle the pagination loop. This code is stateful, harder to read, and not composable.\n\n```typescript\nimport { Effect, Chunk, Option } from \"effect\";\n// ... same mock API setup ...\n\nconst fetchAllUsersManually = (): Effect.Effect, Error> =>\n Effect.gen(function* () {\n // Manual state management for results and current page\n let allFetchedUsers: User[] = [];\n let currentPage: Option.Option = Option.some(1);\n\n // Manual loop to fetch pages\n while (Option.isSome(currentPage)) {\n const [users, nextPage] = yield* fetchUsersPage(currentPage.value);\n allFetchedUsers = allFetchedUsers.concat(Chunk.toArray(users));\n currentPage = nextPage;\n }\n\n return Chunk.fromIterable(allFetchedUsers);\n });\n\nconst program = fetchAllUsersManually().pipe(\n Effect.map((users) => users.length)\n);\n\nEffect.runPromise(program).then((totalUsers) => {\n console.log(`Total users fetched from all pages: ${totalUsers}`);\n});\n```\n\nThis manual approach is inferior because it forces you to manage state explicitly (`allFetchedUsers`, `currentPage`). The logic is contained within a single, monolithic effect that is not lazy and cannot be easily composed with other stream operators without first collecting all results. `Stream.paginateEffect` abstracts away this entire block of boilerplate code." - }, - { - "id": "data-class", - "title": "Type Classes for Equality, Ordering, and Hashing with Data.Class", - "description": "Use Data.Class to define and derive type classes for your data types, supporting composable equality, ordering, and hashing.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Data, Equal, HashSet } from \"effect\";\n\n// Define custom data types with structural equality\nconst user1 = Data.struct({ id: 1, name: \"Alice\" });\nconst user2 = Data.struct({ id: 1, name: \"Alice\" });\nconst user3 = Data.struct({ id: 2, name: \"Bob\" });\n\n// Data.struct provides automatic structural equality\nconsole.log(Equal.equals(user1, user2)); // true (same structure)\nconsole.log(Equal.equals(user1, user3)); // false (different values)\n\n// Use in a HashSet (works because Data.struct implements Equal)\nconst set = HashSet.make(user1);\nconsole.log(HashSet.has(set, user2)); // true (structural equality)\n\n// Create an array and use structural equality\nconst users = [user1, user3];\nconsole.log(users.some((u) => Equal.equals(u, user2))); // true\n```\n\n**Explanation:**\n\n- `Data.Class.getEqual` derives an equality type class for your data type.\n- `Data.Class.getOrder` derives an ordering type class, useful for sorting.\n- `Data.Class.getHash` derives a hash function for use in sets and maps.\n- These type classes make your types fully compatible with Effect’s collections and algorithms.", - "antiPattern": "Relying on reference equality, ad-hoc comparison functions, or not providing type class instances for your custom types, which can lead to bugs and inconsistent behavior in collections.", - "explanation": "Type classes like `Equal`, `Order`, and `Hash` provide a principled way to define how your types are compared, ordered, and hashed. \nThis is essential for using your types in sets, maps, and for sorting or deduplication.", - "content": "# Type Classes for Equality, Ordering, and Hashing with `Data.Class`\n\n## Guideline\n\nUse `Data.Class` to derive or implement type classes for equality, ordering, and hashing for your custom data types. \nThis enables composable, type-safe abstractions and allows your types to work seamlessly with Effect’s collections and algorithms.\n\n## Rationale\n\nType classes like `Equal`, `Order`, and `Hash` provide a principled way to define how your types are compared, ordered, and hashed. \nThis is essential for using your types in sets, maps, and for sorting or deduplication.\n\n## Good Example\n\n```typescript\nimport { Data, Equal, HashSet } from \"effect\";\n\n// Define custom data types with structural equality\nconst user1 = Data.struct({ id: 1, name: \"Alice\" });\nconst user2 = Data.struct({ id: 1, name: \"Alice\" });\nconst user3 = Data.struct({ id: 2, name: \"Bob\" });\n\n// Data.struct provides automatic structural equality\nconsole.log(Equal.equals(user1, user2)); // true (same structure)\nconsole.log(Equal.equals(user1, user3)); // false (different values)\n\n// Use in a HashSet (works because Data.struct implements Equal)\nconst set = HashSet.make(user1);\nconsole.log(HashSet.has(set, user2)); // true (structural equality)\n\n// Create an array and use structural equality\nconst users = [user1, user3];\nconsole.log(users.some((u) => Equal.equals(u, user2))); // true\n```\n\n**Explanation:**\n\n- `Data.Class.getEqual` derives an equality type class for your data type.\n- `Data.Class.getOrder` derives an ordering type class, useful for sorting.\n- `Data.Class.getHash` derives a hash function for use in sets and maps.\n- These type classes make your types fully compatible with Effect’s collections and algorithms.\n\n## Anti-Pattern\n\nRelying on reference equality, ad-hoc comparison functions, or not providing type class instances for your custom types, which can lead to bugs and inconsistent behavior in collections." - }, - { - "id": "understand-fibers-as-lightweight-threads", - "title": "Understand Fibers as Lightweight Threads", - "description": "Understand that a Fiber is a lightweight, virtual thread managed by the Effect runtime for massive concurrency.", - "skillLevel": "advanced", - "useCases": [ - "concurrency" - ], - "example": "This program demonstrates the efficiency of fibers by forking 100,000 of them. Each fiber does a small amount of work (sleeping for 1 second). Trying to do this with 100,000 OS threads would instantly crash any system.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst program = Effect.gen(function* () {\n // Demonstrate the lightweight nature of fibers by creating 100,000 of them\n // This would be impossible with OS threads due to memory and context switching overhead\n const fiberCount = 100_000;\n yield* Effect.log(`Forking ${fiberCount} fibers...`);\n\n // Create an array of 100,000 simple effects\n // Each effect sleeps for 1 second and then returns its index\n // This simulates lightweight concurrent tasks\n const tasks = Array.from({ length: fiberCount }, (_, i) =>\n Effect.sleep(\"1 second\").pipe(Effect.as(i))\n );\n\n // Fork all of them into background fibers\n // Effect.fork creates a new fiber for each task without blocking\n // This demonstrates fiber creation scalability - 100k fibers created almost instantly\n // Each fiber is much lighter than an OS thread (typically ~1KB vs ~8MB per thread)\n const fibers = yield* Effect.forEach(tasks, Effect.fork);\n\n yield* Effect.log(\n \"All fibers have been forked. Now waiting for them to complete...\"\n );\n\n // Wait for all fibers to finish their work\n // Fiber.joinAll waits for all fibers to complete and collects their results\n // This demonstrates fiber coordination - managing thousands of concurrent operations\n // The runtime efficiently schedules these fibers using a work-stealing thread pool\n const results = yield* Fiber.joinAll(fibers);\n\n yield* Effect.log(`All ${results.length} fibers have completed.`);\n\n // Key insights from this example:\n // 1. Fibers are extremely lightweight - 100k fibers use minimal memory\n // 2. Fiber creation is fast - no expensive OS thread allocation\n // 3. The Effect runtime efficiently schedules fibers across available CPU cores\n // 4. Fibers can be suspended and resumed without blocking OS threads\n // 5. This enables massive concurrency for I/O-bound operations\n});\n\n// This program runs successfully, demonstrating the low overhead of fibers.\n// Try running this with OS threads - you'd likely hit system limits around 1000-10000 threads\n// With fibers, 100k+ concurrent operations are easily achievable\nEffect.runPromise(program);\n```\n\n---", - "antiPattern": "The anti-pattern is thinking that a `Fiber` is the same as an OS thread. This can lead to incorrect assumptions about performance and behavior.\n\n- **Don't assume parallelism on CPU-bound tasks:** In a standard Node.js environment, all fibers run on a single OS thread. If you run 10 CPU-intensive tasks on 10 fibers, they will not run in parallel on 10 different CPU cores. They will share time on the single main thread. Fibers provide massive concurrency for I/O-bound tasks (like network requests), not CPU-bound parallelism.\n- **Don't worry about blocking:** A `Fiber` that is \"sleeping\" or waiting for I/O (like `Effect.sleep` or a `fetch` request) does not block the underlying OS thread. The Effect runtime simply puts it aside and uses the thread to run other ready fibers.", - "explanation": "In traditional multi-threaded programming, each thread is managed by the operating system, consumes significant memory (for its stack), and involves expensive context switching. This limits the number of concurrent threads you can realistically create.\n\nEffect's `Fiber`s are different. They are managed entirely by the Effect runtime, not the OS. They are incredibly lightweight data structures that don't have their own OS thread stack. The Effect runtime uses a cooperative scheduling mechanism to run many fibers on a small pool of OS threads (often just one in Node.js).\n\nThis model, known as M:N threading (M fibers on N OS threads), allows for a massive level of concurrency that is impossible with traditional threads. It's what makes Effect so powerful for building highly concurrent applications like servers, data pipelines, and real-time systems.\n\nWhen you use operators like `Effect.fork` or `Effect.all`, you are creating new fibers.\n\n---", - "content": "## Guideline\n\nThink of a `Fiber` as a \"virtual thread\" or a \"green thread.\" It is the fundamental unit of concurrency in Effect. Every `Effect` you run is executed on a `Fiber`. Unlike OS threads, which are heavy and limited, you can create hundreds of thousands or even millions of fibers without issue.\n\n---\n\n## Rationale\n\nIn traditional multi-threaded programming, each thread is managed by the operating system, consumes significant memory (for its stack), and involves expensive context switching. This limits the number of concurrent threads you can realistically create.\n\nEffect's `Fiber`s are different. They are managed entirely by the Effect runtime, not the OS. They are incredibly lightweight data structures that don't have their own OS thread stack. The Effect runtime uses a cooperative scheduling mechanism to run many fibers on a small pool of OS threads (often just one in Node.js).\n\nThis model, known as M:N threading (M fibers on N OS threads), allows for a massive level of concurrency that is impossible with traditional threads. It's what makes Effect so powerful for building highly concurrent applications like servers, data pipelines, and real-time systems.\n\nWhen you use operators like `Effect.fork` or `Effect.all`, you are creating new fibers.\n\n---\n\n## Good Example\n\nThis program demonstrates the efficiency of fibers by forking 100,000 of them. Each fiber does a small amount of work (sleeping for 1 second). Trying to do this with 100,000 OS threads would instantly crash any system.\n\n```typescript\nimport { Effect, Fiber } from \"effect\";\n\nconst program = Effect.gen(function* () {\n // Demonstrate the lightweight nature of fibers by creating 100,000 of them\n // This would be impossible with OS threads due to memory and context switching overhead\n const fiberCount = 100_000;\n yield* Effect.log(`Forking ${fiberCount} fibers...`);\n\n // Create an array of 100,000 simple effects\n // Each effect sleeps for 1 second and then returns its index\n // This simulates lightweight concurrent tasks\n const tasks = Array.from({ length: fiberCount }, (_, i) =>\n Effect.sleep(\"1 second\").pipe(Effect.as(i))\n );\n\n // Fork all of them into background fibers\n // Effect.fork creates a new fiber for each task without blocking\n // This demonstrates fiber creation scalability - 100k fibers created almost instantly\n // Each fiber is much lighter than an OS thread (typically ~1KB vs ~8MB per thread)\n const fibers = yield* Effect.forEach(tasks, Effect.fork);\n\n yield* Effect.log(\n \"All fibers have been forked. Now waiting for them to complete...\"\n );\n\n // Wait for all fibers to finish their work\n // Fiber.joinAll waits for all fibers to complete and collects their results\n // This demonstrates fiber coordination - managing thousands of concurrent operations\n // The runtime efficiently schedules these fibers using a work-stealing thread pool\n const results = yield* Fiber.joinAll(fibers);\n\n yield* Effect.log(`All ${results.length} fibers have completed.`);\n\n // Key insights from this example:\n // 1. Fibers are extremely lightweight - 100k fibers use minimal memory\n // 2. Fiber creation is fast - no expensive OS thread allocation\n // 3. The Effect runtime efficiently schedules fibers across available CPU cores\n // 4. Fibers can be suspended and resumed without blocking OS threads\n // 5. This enables massive concurrency for I/O-bound operations\n});\n\n// This program runs successfully, demonstrating the low overhead of fibers.\n// Try running this with OS threads - you'd likely hit system limits around 1000-10000 threads\n// With fibers, 100k+ concurrent operations are easily achievable\nEffect.runPromise(program);\n```\n\n---\n\n## Anti-Pattern: Mental Model Mismatch\n\nThe anti-pattern is thinking that a `Fiber` is the same as an OS thread. This can lead to incorrect assumptions about performance and behavior.\n\n- **Don't assume parallelism on CPU-bound tasks:** In a standard Node.js environment, all fibers run on a single OS thread. If you run 10 CPU-intensive tasks on 10 fibers, they will not run in parallel on 10 different CPU cores. They will share time on the single main thread. Fibers provide massive concurrency for I/O-bound tasks (like network requests), not CPU-bound parallelism.\n- **Don't worry about blocking:** A `Fiber` that is \"sleeping\" or waiting for I/O (like `Effect.sleep` or a `fetch` request) does not block the underlying OS thread. The Effect runtime simply puts it aside and uses the thread to run other ready fibers." - }, - { - "id": "understand-layers-for-dependency-injection", - "title": "Understand Layers for Dependency Injection", - "description": "Understand that a Layer is a blueprint describing how to construct a service and its dependencies.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "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.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define the Logger service with a default implementation\nexport class Logger extends Effect.Service()(\"Logger\", {\n // Provide a synchronous implementation\n sync: () => ({\n log: (msg: string) => Effect.log(`LOG: ${msg}`),\n }),\n}) {}\n\n// Define the Notifier service that depends on Logger\nexport class Notifier extends Effect.Service()(\"Notifier\", {\n // Provide an implementation that requires Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n notify: (msg: string) => logger.log(`Notifying: ${msg}`),\n };\n }),\n // Specify dependencies\n dependencies: [Logger.Default],\n}) {}\n\n// Create a program that uses both services\nconst program = Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notify(\"Hello, World!\");\n});\n\n// Run the program with the default implementations\nEffect.runPromise(Effect.provide(program, Notifier.Default));\n```\n\n---", - "antiPattern": "Manually creating and passing service instances around. This is the \"poor man's DI\" and leads to tightly coupled code that is difficult to test and maintain.\n\n```typescript\n// ❌ WRONG: Manual instantiation and prop-drilling.\nclass LoggerImpl {\n log(msg: string) {\n console.log(msg);\n }\n}\n\nclass NotifierImpl {\n constructor(private logger: LoggerImpl) {}\n notify(msg: string) {\n this.logger.log(msg);\n }\n}\n\n// Dependencies must be created and passed in manually.\nconst logger = new LoggerImpl();\nconst notifier = new NotifierImpl(logger);\n\n// This is not easily testable without creating real instances.\nnotifier.notify(\"Hello\");\n```", - "explanation": "In Effect, you don't create service instances directly. Instead, you define `Layer`s that describe _how_ to create them. This separation of declaration from implementation is the core of Effect's powerful dependency injection (DI) system.\n\nThis approach has several key benefits:\n\n- **Composability:** You can combine small, focused layers into a complete application layer (`Layer.merge`, `Layer.provide`).\n- **Declarative Dependencies:** A layer's type signature explicitly documents its own dependencies, making your application's architecture clear and self-documenting.\n- **Testability:** For testing, you can easily swap a \"live\" layer (e.g., one that connects to a real database) with a \"test\" layer (one that provides mock data) without changing any of your business logic.\n\n---", - "content": "## Guideline\n\nThink of a `Layer` as a recipe for building a service. It's a declarative blueprint that specifies:\n\n- **`A` (Output)**: The service it provides (e.g., `HttpClient`).\n- **`R` (Requirements)**: The other services it needs to be built (e.g., `ConfigService`).\n- **`E` (Error)**: The errors that could occur during its construction (e.g., `ConfigError`).\n\n---\n\n## Rationale\n\nIn Effect, you don't create service instances directly. Instead, you define `Layer`s that describe _how_ to create them. This separation of declaration from implementation is the core of Effect's powerful dependency injection (DI) system.\n\nThis approach has several key benefits:\n\n- **Composability:** You can combine small, focused layers into a complete application layer (`Layer.merge`, `Layer.provide`).\n- **Declarative Dependencies:** A layer's type signature explicitly documents its own dependencies, making your application's architecture clear and self-documenting.\n- **Testability:** For testing, you can easily swap a \"live\" layer (e.g., one that connects to a real database) with a \"test\" layer (one that provides mock data) without changing any of your business logic.\n\n---\n\n## Good Example\n\nHere, we define a `Notifier` service that requires a `Logger` to be built. The `NotifierLive` layer's type signature, `Layer`, clearly documents this dependency.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define the Logger service with a default implementation\nexport class Logger extends Effect.Service()(\"Logger\", {\n // Provide a synchronous implementation\n sync: () => ({\n log: (msg: string) => Effect.log(`LOG: ${msg}`),\n }),\n}) {}\n\n// Define the Notifier service that depends on Logger\nexport class Notifier extends Effect.Service()(\"Notifier\", {\n // Provide an implementation that requires Logger\n effect: Effect.gen(function* () {\n const logger = yield* Logger;\n return {\n notify: (msg: string) => logger.log(`Notifying: ${msg}`),\n };\n }),\n // Specify dependencies\n dependencies: [Logger.Default],\n}) {}\n\n// Create a program that uses both services\nconst program = Effect.gen(function* () {\n const notifier = yield* Notifier;\n yield* notifier.notify(\"Hello, World!\");\n});\n\n// Run the program with the default implementations\nEffect.runPromise(Effect.provide(program, Notifier.Default));\n```\n\n---\n\n## Anti-Pattern\n\nManually creating and passing service instances around. This is the \"poor man's DI\" and leads to tightly coupled code that is difficult to test and maintain.\n\n```typescript\n// ❌ WRONG: Manual instantiation and prop-drilling.\nclass LoggerImpl {\n log(msg: string) {\n console.log(msg);\n }\n}\n\nclass NotifierImpl {\n constructor(private logger: LoggerImpl) {}\n notify(msg: string) {\n this.logger.log(msg);\n }\n}\n\n// Dependencies must be created and passed in manually.\nconst logger = new LoggerImpl();\nconst notifier = new NotifierImpl(logger);\n\n// This is not easily testable without creating real instances.\nnotifier.notify(\"Hello\");\n```" - }, - { - "id": "effects-are-lazy", - "title": "Understand that Effects are Lazy Blueprints", - "description": "Understand that effects are lazy blueprints.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nEffect.runSync(Effect.log(\"1. Defining the Effect blueprint...\"));\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"3. The blueprint is now being executed!\");\n return 42;\n});\n\nconst demonstrationProgram = Effect.gen(function* () {\n yield* Effect.log(\n \"2. The blueprint has been defined. No work has been done yet.\"\n );\n yield* program;\n});\n\nEffect.runSync(demonstrationProgram);\n```\n\n**Explanation:** \nDefining an `Effect` does not execute any code inside it. Only when you call\n`Effect.runSync(program)` does the computation actually happen.", - "antiPattern": "Assuming an `Effect` behaves like a `Promise`. A `Promise` executes its work\nimmediately upon creation. Never expect a side effect to occur just from\ndefining an `Effect`.", - "explanation": "This laziness is a superpower because it makes your code composable,\npredictable, and testable. Unlike a `Promise` which executes immediately,\nan `Effect` is just a description of work, like a recipe waiting for a chef.", - "content": "# Understand that Effects are Lazy Blueprints\n\n## Guideline\n\nAn `Effect` is not a value or a `Promise`. It is a lazy, immutable blueprint\nthat describes a computation. It does nothing on its own until it is passed to\na runtime executor (e.g., `Effect.runPromise` or `Effect.runSync`).\n\n## Rationale\n\nThis laziness is a superpower because it makes your code composable,\npredictable, and testable. Unlike a `Promise` which executes immediately,\nan `Effect` is just a description of work, like a recipe waiting for a chef.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nEffect.runSync(Effect.log(\"1. Defining the Effect blueprint...\"));\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"3. The blueprint is now being executed!\");\n return 42;\n});\n\nconst demonstrationProgram = Effect.gen(function* () {\n yield* Effect.log(\n \"2. The blueprint has been defined. No work has been done yet.\"\n );\n yield* program;\n});\n\nEffect.runSync(demonstrationProgram);\n```\n\n**Explanation:** \nDefining an `Effect` does not execute any code inside it. Only when you call\n`Effect.runSync(program)` does the computation actually happen.\n\n## Anti-Pattern\n\nAssuming an `Effect` behaves like a `Promise`. A `Promise` executes its work\nimmediately upon creation. Never expect a side effect to occur just from\ndefining an `Effect`." - }, - { - "id": "understand-effect-channels", - "title": "Understand the Three Effect Channels (A, E, R)", - "description": "Understand that an Effect<A, E, R> describes a computation with a success type (A), an error type (E), and a requirements type (R).", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "This function signature is a self-documenting contract. It clearly states that to get a `User`, you must provide a `Database` service, and the operation might fail with a `UserNotFoundError`.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define the types for our channels\ninterface User {\n readonly name: string;\n} // The 'A' type\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\") {} // The 'E' type\n\n// Define the Database service using Effect.Service\nexport class Database extends Effect.Service()(\"Database\", {\n // Provide a default implementation\n sync: () => ({\n findUser: (id: number) =>\n id === 1\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError()),\n }),\n}) {}\n\n// This function's signature shows all three channels\nconst getUser = (\n id: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const db = yield* Database;\n return yield* db.findUser(id);\n });\n\n// The program will use the default implementation\nconst program = getUser(1);\n\n// Run the program with the default implementation\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* Effect.provide(program, Database.Default);\n yield* Effect.log(`Result: ${JSON.stringify(result)}`); // { name: 'Paul' }\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---", - "antiPattern": "Ignoring the type system and using generic types. This throws away all the safety and clarity that Effect provides.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This signature is dishonest and unsafe.\n// It hides the dependency on a database and the possibility of failure.\nfunction getUserUnsafely(id: number, db: any): Effect.Effect {\n try {\n const user = db.findUser(id);\n if (!user) {\n // This will be an unhandled defect, not a typed error.\n throw new Error(\"User not found\");\n }\n return Effect.succeed(user);\n } catch (e) {\n // This is also an untyped failure.\n return Effect.fail(e);\n }\n}\n```", - "explanation": "This three-channel signature is what makes Effect so expressive and safe. Unlike a `Promise` which can only describe its success type, an `Effect`'s signature tells you everything you need to know about a computation before you run it:\n\n1. **What it produces (`A`):** The data you get on the \"happy path.\"\n2. **How it can fail (`E`):** The specific, known errors you need to handle. This makes error handling type-safe and explicit, unlike throwing generic `Error`s.\n3. **What it needs (`R`):** The \"ingredients\" or dependencies required to run the effect. This is the foundation of Effect's powerful dependency injection system. An `Effect` can only be executed when its `R` channel is `never`, meaning all its dependencies have been provided.\n\nThis turns the TypeScript compiler into a powerful assistant that ensures you've handled all possible outcomes and provided all necessary dependencies.\n\n---", - "content": "## Guideline\n\nEvery `Effect` has three generic type parameters: `Effect` which represent its three \"channels\":\n\n- **`A` (Success Channel):** The type of value the `Effect` will produce if it succeeds.\n- **`E` (Error/Failure Channel):** The type of error the `Effect` can fail with. These are expected, recoverable errors.\n- **`R` (Requirement/Context Channel):** The services or dependencies the `Effect` needs to run.\n\n---\n\n## Rationale\n\nThis three-channel signature is what makes Effect so expressive and safe. Unlike a `Promise` which can only describe its success type, an `Effect`'s signature tells you everything you need to know about a computation before you run it:\n\n1. **What it produces (`A`):** The data you get on the \"happy path.\"\n2. **How it can fail (`E`):** The specific, known errors you need to handle. This makes error handling type-safe and explicit, unlike throwing generic `Error`s.\n3. **What it needs (`R`):** The \"ingredients\" or dependencies required to run the effect. This is the foundation of Effect's powerful dependency injection system. An `Effect` can only be executed when its `R` channel is `never`, meaning all its dependencies have been provided.\n\nThis turns the TypeScript compiler into a powerful assistant that ensures you've handled all possible outcomes and provided all necessary dependencies.\n\n---\n\n## Good Example\n\nThis function signature is a self-documenting contract. It clearly states that to get a `User`, you must provide a `Database` service, and the operation might fail with a `UserNotFoundError`.\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define the types for our channels\ninterface User {\n readonly name: string;\n} // The 'A' type\nclass UserNotFoundError extends Data.TaggedError(\"UserNotFoundError\") {} // The 'E' type\n\n// Define the Database service using Effect.Service\nexport class Database extends Effect.Service()(\"Database\", {\n // Provide a default implementation\n sync: () => ({\n findUser: (id: number) =>\n id === 1\n ? Effect.succeed({ name: \"Paul\" })\n : Effect.fail(new UserNotFoundError()),\n }),\n}) {}\n\n// This function's signature shows all three channels\nconst getUser = (\n id: number\n): Effect.Effect =>\n Effect.gen(function* () {\n const db = yield* Database;\n return yield* db.findUser(id);\n });\n\n// The program will use the default implementation\nconst program = getUser(1);\n\n// Run the program with the default implementation\nconst programWithLogging = Effect.gen(function* () {\n const result = yield* Effect.provide(program, Database.Default);\n yield* Effect.log(`Result: ${JSON.stringify(result)}`); // { name: 'Paul' }\n return result;\n});\n\nEffect.runPromise(programWithLogging);\n```\n\n---\n\n## Anti-Pattern\n\nIgnoring the type system and using generic types. This throws away all the safety and clarity that Effect provides.\n\n```typescript\nimport { Effect } from \"effect\";\n\n// ❌ WRONG: This signature is dishonest and unsafe.\n// It hides the dependency on a database and the possibility of failure.\nfunction getUserUnsafely(id: number, db: any): Effect.Effect {\n try {\n const user = db.findUser(id);\n if (!user) {\n // This will be an unhandled defect, not a typed error.\n throw new Error(\"User not found\");\n }\n return Effect.succeed(user);\n } catch (e) {\n // This is also an untyped failure.\n return Effect.fail(e);\n }\n}\n```" - }, - { - "id": "concurrency-understanding-fibers", - "title": "Understanding Fibers", - "description": "Fibers are lightweight threads managed by Effect, enabling efficient concurrency without OS thread overhead.", - "skillLevel": "beginner", - "useCases": [ - "concurrency-getting-started" - ], - "example": "```typescript\nimport { Effect, Fiber } from \"effect\"\n\n// ============================================\n// WHAT IS A FIBER?\n// ============================================\n\n// A fiber is a running effect. When you run an effect,\n// it executes on a fiber.\n\nconst myEffect = Effect.gen(function* () {\n yield* Effect.log(\"Hello from a fiber!\")\n yield* Effect.sleep(\"100 millis\")\n return 42\n})\n\n// This runs myEffect on the \"main\" fiber\nEffect.runPromise(myEffect)\n\n// ============================================\n// FORKING: Create a new fiber\n// ============================================\n\nconst withFork = Effect.gen(function* () {\n yield* Effect.log(\"Main fiber starting\")\n \n // Fork creates a new fiber that runs independently\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Effect.log(\"Child fiber running\")\n yield* Effect.sleep(\"200 millis\")\n yield* Effect.log(\"Child fiber done\")\n return \"child result\"\n })\n )\n \n yield* Effect.log(\"Main fiber continues immediately\")\n yield* Effect.sleep(\"100 millis\")\n yield* Effect.log(\"Main fiber waiting for child...\")\n \n // Wait for the forked fiber to complete\n const result = yield* Fiber.join(fiber)\n yield* Effect.log(`Got result: ${result}`)\n})\n\nEffect.runPromise(withFork)\n/*\nOutput:\nMain fiber starting\nChild fiber running\nMain fiber continues immediately\nMain fiber waiting for child...\nChild fiber done\nGot result: child result\n*/\n\n// ============================================\n// FIBER OPERATIONS\n// ============================================\n\nconst fiberOps = Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Effect.sleep(\"1 second\")\n return \"done\"\n })\n )\n \n // Check if fiber is done (non-blocking)\n const poll = yield* Fiber.poll(fiber)\n yield* Effect.log(`Poll result: ${poll}`) // None (still running)\n \n // Wait for completion\n const result = yield* Fiber.join(fiber)\n yield* Effect.log(`Join result: ${result}`)\n \n // Or interrupt if taking too long\n // yield* Fiber.interrupt(fiber)\n})\n```", - "antiPattern": "", - "explanation": "Unlike OS threads:\n\n1. **Lightweight** - Create thousands without performance issues\n2. **Cooperative** - Yield control at effect boundaries\n3. **Interruptible** - Can be cancelled cleanly\n4. **Structured** - Parent fibers manage children\n\n---", - "content": "## Guideline\n\nFibers are Effect's lightweight threads. They're cheap to create (thousands are fine), automatically managed, and can be interrupted cleanly.\n\n---\n\n## Rationale\n\nUnlike OS threads:\n\n1. **Lightweight** - Create thousands without performance issues\n2. **Cooperative** - Yield control at effect boundaries\n3. **Interruptible** - Can be cancelled cleanly\n4. **Structured** - Parent fibers manage children\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Fiber } from \"effect\"\n\n// ============================================\n// WHAT IS A FIBER?\n// ============================================\n\n// A fiber is a running effect. When you run an effect,\n// it executes on a fiber.\n\nconst myEffect = Effect.gen(function* () {\n yield* Effect.log(\"Hello from a fiber!\")\n yield* Effect.sleep(\"100 millis\")\n return 42\n})\n\n// This runs myEffect on the \"main\" fiber\nEffect.runPromise(myEffect)\n\n// ============================================\n// FORKING: Create a new fiber\n// ============================================\n\nconst withFork = Effect.gen(function* () {\n yield* Effect.log(\"Main fiber starting\")\n \n // Fork creates a new fiber that runs independently\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Effect.log(\"Child fiber running\")\n yield* Effect.sleep(\"200 millis\")\n yield* Effect.log(\"Child fiber done\")\n return \"child result\"\n })\n )\n \n yield* Effect.log(\"Main fiber continues immediately\")\n yield* Effect.sleep(\"100 millis\")\n yield* Effect.log(\"Main fiber waiting for child...\")\n \n // Wait for the forked fiber to complete\n const result = yield* Fiber.join(fiber)\n yield* Effect.log(`Got result: ${result}`)\n})\n\nEffect.runPromise(withFork)\n/*\nOutput:\nMain fiber starting\nChild fiber running\nMain fiber continues immediately\nMain fiber waiting for child...\nChild fiber done\nGot result: child result\n*/\n\n// ============================================\n// FIBER OPERATIONS\n// ============================================\n\nconst fiberOps = Effect.gen(function* () {\n const fiber = yield* Effect.fork(\n Effect.gen(function* () {\n yield* Effect.sleep(\"1 second\")\n return \"done\"\n })\n )\n \n // Check if fiber is done (non-blocking)\n const poll = yield* Fiber.poll(fiber)\n yield* Effect.log(`Poll result: ${poll}`) // None (still running)\n \n // Wait for completion\n const result = yield* Fiber.join(fiber)\n yield* Effect.log(`Join result: ${result}`)\n \n // Or interrupt if taking too long\n // yield* Fiber.interrupt(fiber)\n})\n```\n\n## Fiber vs Thread\n\n| Aspect | OS Thread | Effect Fiber |\n|--------|-----------|--------------|\n| Memory | ~1MB stack | ~few KB |\n| Creation | Expensive | Cheap |\n| Scheduling | OS kernel | Effect runtime |\n| Interruption | Messy | Clean |\n| Count | Hundreds | Thousands |\n\n## Key Operations\n\n| Operation | What it does |\n|-----------|--------------|\n| `Effect.fork(effect)` | Start effect on new fiber |\n| `Fiber.join(fiber)` | Wait for fiber to complete |\n| `Fiber.interrupt(fiber)` | Cancel the fiber |\n| `Fiber.poll(fiber)` | Check status without waiting |\n| `Fiber.await(fiber)` | Get Exit (success or failure) |\n\n## Mental Model\n\nThink of fibers as \"lightweight async tasks\":\n\n```\nMain Fiber\n │\n ├── fork ──► Child Fiber 1 ──► runs independently\n │\n ├── fork ──► Child Fiber 2 ──► runs independently\n │\n └── join ◄── waits for children\n```" - }, - { - "id": "use-pipe-for-composition", - "title": "Use .pipe for Composition", - "description": "Use .pipe for composition.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.succeed(5).pipe(\n Effect.map((n) => n * 2),\n Effect.map((n) => `The result is ${n}`),\n Effect.tap(Effect.log)\n);\n\n// Demonstrate various pipe composition patterns\nconst demo = Effect.gen(function* () {\n yield* Effect.log(\"=== Using Pipe for Composition Demo ===\");\n\n // 1. Basic pipe composition\n yield* Effect.log(\"\\n1. Basic pipe composition:\");\n yield* program;\n\n // 2. Complex pipe composition with multiple transformations\n yield* Effect.log(\"\\n2. Complex pipe composition:\");\n const complexResult = yield* Effect.succeed(10).pipe(\n Effect.map((n) => n + 5),\n Effect.map((n) => n * 2),\n Effect.tap((n) => Effect.log(`Intermediate result: ${n}`)),\n Effect.map((n) => n.toString()),\n Effect.map((s) => `Final: ${s}`)\n );\n yield* Effect.log(\"Complex result: \" + complexResult);\n\n // 3. Pipe with flatMap for chaining effects\n yield* Effect.log(\"\\n3. Pipe with flatMap for chaining effects:\");\n const chainedResult = yield* Effect.succeed(\"hello\").pipe(\n Effect.map((s) => s.toUpperCase()),\n Effect.flatMap((s) => Effect.succeed(`${s} WORLD`)),\n Effect.flatMap((s) => Effect.succeed(`${s}!`)),\n Effect.tap((s) => Effect.log(`Chained: ${s}`))\n );\n yield* Effect.log(\"Chained result: \" + chainedResult);\n\n // 4. Pipe with error handling\n yield* Effect.log(\"\\n4. Pipe with error handling:\");\n const errorHandledResult = yield* Effect.succeed(-1).pipe(\n Effect.flatMap((n) =>\n n > 0 ? Effect.succeed(n) : Effect.fail(new Error(\"Negative number\"))\n ),\n Effect.catchAll((error) =>\n Effect.succeed(\"Handled error: \" + error.message)\n ),\n Effect.tap((result) => Effect.log(`Error handled: ${result}`))\n );\n yield* Effect.log(\"Error handled result: \" + errorHandledResult);\n\n // 5. Pipe with multiple operations\n yield* Effect.log(\"\\n5. Pipe with multiple operations:\");\n const multiOpResult = yield* Effect.succeed([1, 2, 3, 4, 5]).pipe(\n Effect.map((arr) => arr.filter((n) => n % 2 === 0)),\n Effect.map((arr) => arr.map((n) => n * 2)),\n Effect.map((arr) => arr.reduce((sum, n) => sum + n, 0)),\n Effect.tap((sum) => Effect.log(`Sum of even numbers doubled: ${sum}`))\n );\n yield* Effect.log(\"Multi-operation result: \" + multiOpResult);\n\n yield* Effect.log(\"\\n✅ Pipe composition demonstration completed!\");\n});\n\nEffect.runPromise(demo);\n```\n\n**Explanation:** \nUsing `.pipe()` allows you to compose operations in a top-to-bottom style,\nimproving readability and maintainability.", - "antiPattern": "Nesting function calls manually. This is hard to read and reorder.\n`Effect.tap(Effect.map(Effect.map(Effect.succeed(5), n => n * 2), n => ...))`", - "explanation": "Piping makes code readable and avoids deeply nested function calls. It allows\nyou to see the flow of data transformations in a clear, linear fashion.", - "content": "# Use .pipe for Composition\n\n## Guideline\n\nTo apply a sequence of transformations or operations to an `Effect`, use the\n`.pipe()` method.\n\n## Rationale\n\nPiping makes code readable and avoids deeply nested function calls. It allows\nyou to see the flow of data transformations in a clear, linear fashion.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst program = Effect.succeed(5).pipe(\n Effect.map((n) => n * 2),\n Effect.map((n) => `The result is ${n}`),\n Effect.tap(Effect.log)\n);\n\n// Demonstrate various pipe composition patterns\nconst demo = Effect.gen(function* () {\n yield* Effect.log(\"=== Using Pipe for Composition Demo ===\");\n\n // 1. Basic pipe composition\n yield* Effect.log(\"\\n1. Basic pipe composition:\");\n yield* program;\n\n // 2. Complex pipe composition with multiple transformations\n yield* Effect.log(\"\\n2. Complex pipe composition:\");\n const complexResult = yield* Effect.succeed(10).pipe(\n Effect.map((n) => n + 5),\n Effect.map((n) => n * 2),\n Effect.tap((n) => Effect.log(`Intermediate result: ${n}`)),\n Effect.map((n) => n.toString()),\n Effect.map((s) => `Final: ${s}`)\n );\n yield* Effect.log(\"Complex result: \" + complexResult);\n\n // 3. Pipe with flatMap for chaining effects\n yield* Effect.log(\"\\n3. Pipe with flatMap for chaining effects:\");\n const chainedResult = yield* Effect.succeed(\"hello\").pipe(\n Effect.map((s) => s.toUpperCase()),\n Effect.flatMap((s) => Effect.succeed(`${s} WORLD`)),\n Effect.flatMap((s) => Effect.succeed(`${s}!`)),\n Effect.tap((s) => Effect.log(`Chained: ${s}`))\n );\n yield* Effect.log(\"Chained result: \" + chainedResult);\n\n // 4. Pipe with error handling\n yield* Effect.log(\"\\n4. Pipe with error handling:\");\n const errorHandledResult = yield* Effect.succeed(-1).pipe(\n Effect.flatMap((n) =>\n n > 0 ? Effect.succeed(n) : Effect.fail(new Error(\"Negative number\"))\n ),\n Effect.catchAll((error) =>\n Effect.succeed(\"Handled error: \" + error.message)\n ),\n Effect.tap((result) => Effect.log(`Error handled: ${result}`))\n );\n yield* Effect.log(\"Error handled result: \" + errorHandledResult);\n\n // 5. Pipe with multiple operations\n yield* Effect.log(\"\\n5. Pipe with multiple operations:\");\n const multiOpResult = yield* Effect.succeed([1, 2, 3, 4, 5]).pipe(\n Effect.map((arr) => arr.filter((n) => n % 2 === 0)),\n Effect.map((arr) => arr.map((n) => n * 2)),\n Effect.map((arr) => arr.reduce((sum, n) => sum + n, 0)),\n Effect.tap((sum) => Effect.log(`Sum of even numbers doubled: ${sum}`))\n );\n yield* Effect.log(\"Multi-operation result: \" + multiOpResult);\n\n yield* Effect.log(\"\\n✅ Pipe composition demonstration completed!\");\n});\n\nEffect.runPromise(demo);\n```\n\n**Explanation:** \nUsing `.pipe()` allows you to compose operations in a top-to-bottom style,\nimproving readability and maintainability.\n\n## Anti-Pattern\n\nNesting function calls manually. This is hard to read and reorder.\n`Effect.tap(Effect.map(Effect.map(Effect.succeed(5), n => n * 2), n => ...))`" - }, - { - "id": "data-chunk", - "title": "Use Chunk for High-Performance Collections", - "description": "Use Chunk to model immutable, high-performance collections for efficient data processing and transformation.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Chunk } from \"effect\";\n\n// Create a Chunk from an array\nconst numbers = Chunk.fromIterable([1, 2, 3, 4]); // Chunk\n\n// Map and filter over a Chunk\nconst doubled = Chunk.map(numbers, (n) => n * 2); // Chunk\nconst evens = Chunk.filter(numbers, (n) => n % 2 === 0); // Chunk\n\n// Concatenate Chunks\nconst moreNumbers = Chunk.fromIterable([5, 6]);\nconst allNumbers = Chunk.appendAll(numbers, moreNumbers); // Chunk\n\n// Convert back to array\nconst arr = Chunk.toArray(allNumbers); // number[]\n```\n\n**Explanation:**\n\n- `Chunk` is immutable and optimized for performance.\n- It supports efficient batch operations, concatenation, and transformation.\n- Use `Chunk` in data pipelines, streaming, and concurrent scenarios.", - "antiPattern": "Using mutable JavaScript arrays for shared or concurrent data, or for large-scale data processing, which can lead to bugs, inefficiency, and unpredictable behavior.", - "explanation": "`Chunk` provides efficient, immutable operations for large or frequently transformed collections. \nIt avoids the pitfalls of mutable arrays and is designed for use in concurrent and streaming workflows.", - "content": "# Use `Chunk` for High-Performance Collections\n\n## Guideline\n\nUse the `Chunk` data type as an immutable, high-performance alternative to JavaScript's `Array`. \n`Chunk` is optimized for functional programming, batch processing, and streaming scenarios.\n\n## Rationale\n\n`Chunk` provides efficient, immutable operations for large or frequently transformed collections. \nIt avoids the pitfalls of mutable arrays and is designed for use in concurrent and streaming workflows.\n\n## Good Example\n\n```typescript\nimport { Chunk } from \"effect\";\n\n// Create a Chunk from an array\nconst numbers = Chunk.fromIterable([1, 2, 3, 4]); // Chunk\n\n// Map and filter over a Chunk\nconst doubled = Chunk.map(numbers, (n) => n * 2); // Chunk\nconst evens = Chunk.filter(numbers, (n) => n % 2 === 0); // Chunk\n\n// Concatenate Chunks\nconst moreNumbers = Chunk.fromIterable([5, 6]);\nconst allNumbers = Chunk.appendAll(numbers, moreNumbers); // Chunk\n\n// Convert back to array\nconst arr = Chunk.toArray(allNumbers); // number[]\n```\n\n**Explanation:**\n\n- `Chunk` is immutable and optimized for performance.\n- It supports efficient batch operations, concatenation, and transformation.\n- Use `Chunk` in data pipelines, streaming, and concurrent scenarios.\n\n## Anti-Pattern\n\nUsing mutable JavaScript arrays for shared or concurrent data, or for large-scale data processing, which can lead to bugs, inefficiency, and unpredictable behavior." - }, - { - "id": "use-chunk-for-high-performance-collections", - "title": "Use Chunk for High-Performance Collections", - "description": "Prefer Chunk over Array for immutable collection operations within data processing pipelines for better performance.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "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.\n\n```typescript\nimport { Chunk, Effect } from \"effect\";\n\n// Create a Chunk from an array\nlet numbers = Chunk.fromIterable([1, 2, 3, 4, 5]);\n\n// Append a new element. This is much faster than [...arr, 6] on large collections.\nnumbers = Chunk.append(numbers, 6);\n\n// Prepend an element.\nnumbers = Chunk.prepend(numbers, 0);\n\n// Take the first 3 elements\nconst firstThree = Chunk.take(numbers, 3);\n\n// Convert back to an array when you need to interface with other libraries\nconst finalArray = Chunk.toReadonlyArray(firstThree);\n\nEffect.runSync(Effect.log(finalArray)); // [0, 1, 2]\n```\n\n---", - "antiPattern": "Eagerly converting a large or potentially infinite iterable to a `Chunk` before streaming. This completely negates the memory-safety benefits of using a `Stream`.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A generator that could produce a very large (or infinite) number of items.\nfunction* largeDataSource() {\n let i = 0;\n while (i < 1_000_000) {\n yield i++;\n }\n}\n\n// ❌ DANGEROUS: `Chunk.fromIterable` will try to pull all 1,000,000 items\n// from the generator and load them into memory at once before the stream\n// even starts. This can lead to high memory usage or a crash.\nconst programWithChunk = Stream.fromChunk(\n Chunk.fromIterable(largeDataSource())\n).pipe(\n Stream.map((n) => n * 2),\n Stream.runDrain\n);\n\n// ✅ CORRECT: `Stream.fromIterable` pulls items from the data source lazily,\n// one at a time (or in small batches), maintaining constant memory usage.\nconst programWithIterable = Stream.fromIterable(largeDataSource()).pipe(\n Stream.map((n) => n * 2),\n Stream.runDrain\n);\n```", - "explanation": "JavaScript's `Array` is a mutable data structure. Every time you perform an \"immutable\" operation like `[...arr, newItem]` or `arr.map(...)`, you are creating a brand new array and copying all the elements from the old one. For small arrays, this is fine. For large arrays or in hot code paths, this constant allocation and copying can become a performance bottleneck.\n\n`Chunk` is designed to solve this. It's an immutable data structure that uses structural sharing internally. When you append an item to a `Chunk`, it doesn't re-copy the entire collection. Instead, it creates a new `Chunk` that reuses most of the internal structure of the original, only allocating memory for the new data. This makes immutable appends and updates significantly faster.\n\n---", - "content": "## Guideline\n\nFor collections that will be heavily transformed with immutable operations (e.g., `map`, `filter`, `append`), use `Chunk`. `Chunk` is Effect's implementation of a persistent and chunked vector that provides better performance than native arrays for these use cases.\n\n---\n\n## Rationale\n\nJavaScript's `Array` is a mutable data structure. Every time you perform an \"immutable\" operation like `[...arr, newItem]` or `arr.map(...)`, you are creating a brand new array and copying all the elements from the old one. For small arrays, this is fine. For large arrays or in hot code paths, this constant allocation and copying can become a performance bottleneck.\n\n`Chunk` is designed to solve this. It's an immutable data structure that uses structural sharing internally. When you append an item to a `Chunk`, it doesn't re-copy the entire collection. Instead, it creates a new `Chunk` that reuses most of the internal structure of the original, only allocating memory for the new data. This makes immutable appends and updates significantly faster.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Chunk, Effect } from \"effect\";\n\n// Create a Chunk from an array\nlet numbers = Chunk.fromIterable([1, 2, 3, 4, 5]);\n\n// Append a new element. This is much faster than [...arr, 6] on large collections.\nnumbers = Chunk.append(numbers, 6);\n\n// Prepend an element.\nnumbers = Chunk.prepend(numbers, 0);\n\n// Take the first 3 elements\nconst firstThree = Chunk.take(numbers, 3);\n\n// Convert back to an array when you need to interface with other libraries\nconst finalArray = Chunk.toReadonlyArray(firstThree);\n\nEffect.runSync(Effect.log(finalArray)); // [0, 1, 2]\n```\n\n---\n\n## Anti-Pattern\n\nEagerly converting a large or potentially infinite iterable to a `Chunk` before streaming. This completely negates the memory-safety benefits of using a `Stream`.\n\n```typescript\nimport { Effect, Stream, Chunk } from \"effect\";\n\n// A generator that could produce a very large (or infinite) number of items.\nfunction* largeDataSource() {\n let i = 0;\n while (i < 1_000_000) {\n yield i++;\n }\n}\n\n// ❌ DANGEROUS: `Chunk.fromIterable` will try to pull all 1,000,000 items\n// from the generator and load them into memory at once before the stream\n// even starts. This can lead to high memory usage or a crash.\nconst programWithChunk = Stream.fromChunk(\n Chunk.fromIterable(largeDataSource())\n).pipe(\n Stream.map((n) => n * 2),\n Stream.runDrain\n);\n\n// ✅ CORRECT: `Stream.fromIterable` pulls items from the data source lazily,\n// one at a time (or in small batches), maintaining constant memory usage.\nconst programWithIterable = Stream.fromIterable(largeDataSource()).pipe(\n Stream.map((n) => n * 2),\n Stream.runDrain\n);\n```" - }, - { - "id": "tooling-devtools", - "title": "Use Effect DevTools", - "description": "Use Effect's built-in debugging features and logging for development.", - "skillLevel": "intermediate", - "useCases": [ - "tooling-and-debugging" - ], - "example": "### 1. Enable Debug Mode\n\n```typescript\nimport { Effect, Logger, LogLevel, FiberRef, Cause } from \"effect\"\n\n// ============================================\n// 1. Verbose logging for development\n// ============================================\n\nconst debugProgram = Effect.gen(function* () {\n yield* Effect.logDebug(\"Starting operation\")\n\n const result = yield* someEffect.pipe(\n Effect.tap((value) => Effect.logDebug(`Got value: ${value}`))\n )\n\n yield* Effect.logDebug(\"Operation complete\")\n return result\n})\n\n// Run with debug logging enabled\nconst runWithDebug = debugProgram.pipe(\n Logger.withMinimumLogLevel(LogLevel.Debug),\n Effect.runPromise\n)\n\n// ============================================\n// 2. Fiber supervision and introspection\n// ============================================\n\nconst inspectFibers = Effect.gen(function* () {\n // Fork some fibers\n const fiber1 = yield* Effect.fork(Effect.sleep(\"1 second\"))\n const fiber2 = yield* Effect.fork(Effect.sleep(\"2 seconds\"))\n\n // Get fiber IDs\n yield* Effect.log(`Fiber 1 ID: ${fiber1.id()}`)\n yield* Effect.log(`Fiber 2 ID: ${fiber2.id()}`)\n\n // Check fiber status\n const status1 = yield* fiber1.status\n yield* Effect.log(`Fiber 1 status: ${status1._tag}`)\n})\n\n// ============================================\n// 3. Trace execution with spans\n// ============================================\n\nconst tracedProgram = Effect.gen(function* () {\n yield* Effect.log(\"=== Starting traced program ===\")\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 1: Initialize\")\n yield* Effect.sleep(\"100 millis\")\n }).pipe(Effect.withLogSpan(\"initialization\"))\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 2: Process\")\n yield* Effect.sleep(\"200 millis\")\n }).pipe(Effect.withLogSpan(\"processing\"))\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 3: Finalize\")\n yield* Effect.sleep(\"50 millis\")\n }).pipe(Effect.withLogSpan(\"finalization\"))\n\n yield* Effect.log(\"=== Program complete ===\")\n})\n\n// ============================================\n// 4. Error cause inspection\n// ============================================\n\nconst debugErrors = Effect.gen(function* () {\n const failingEffect = Effect.gen(function* () {\n yield* Effect.fail(new Error(\"Inner error\"))\n }).pipe(\n Effect.flatMap(() => Effect.fail(new Error(\"Outer error\")))\n )\n\n yield* failingEffect.pipe(\n Effect.catchAllCause((cause) =>\n Effect.gen(function* () {\n yield* Effect.log(\"=== Error Cause Analysis ===\")\n yield* Effect.log(`Pretty printed:\\n${Cause.pretty(cause)}`)\n yield* Effect.log(`Is failure: ${Cause.isFailure(cause)}`)\n yield* Effect.log(`Is interrupted: ${Cause.isInterrupted(cause)}`)\n\n // Extract all failures\n const failures = Cause.failures(cause)\n yield* Effect.log(`Failures: ${JSON.stringify([...failures])}`)\n\n return \"recovered\"\n })\n )\n )\n})\n\n// ============================================\n// 5. Context inspection\n// ============================================\n\nimport { Context } from \"effect\"\n\nclass Config extends Context.Tag(\"Config\")() {}\n\nconst inspectContext = Effect.gen(function* () {\n const context = yield* Effect.context()\n\n yield* Effect.log(\"=== Context Contents ===\")\n yield* Effect.log(`Has Config: ${Context.getOption(context, Config)._tag}`)\n})\n\n// ============================================\n// 6. Custom logger for development\n// ============================================\n\nconst devLogger = Logger.make(({ logLevel, message, date, annotations, spans }) => {\n const timestamp = date.toISOString()\n const level = logLevel.label.padEnd(7)\n const spanInfo = spans.length > 0\n ? ` [${[...spans].map(([name]) => name).join(\" > \")}]`\n : \"\"\n const annotationInfo = Object.keys(annotations).length > 0\n ? ` ${JSON.stringify(Object.fromEntries(annotations))}`\n : \"\"\n\n console.log(`${timestamp} ${level}${spanInfo} ${message}${annotationInfo}`)\n})\n\nconst withDevLogger = (effect: Effect.Effect) =>\n effect.pipe(\n Effect.provide(Logger.replace(Logger.defaultLogger, devLogger))\n )\n\n// ============================================\n// 7. Runtime metrics\n// ============================================\n\nconst showRuntimeMetrics = Effect.gen(function* () {\n const runtime = yield* Effect.runtime()\n\n yield* Effect.log(\"=== Runtime Info ===\")\n // Access runtime configuration\n const fiberRefs = runtime.fiberRefs\n\n yield* Effect.log(\"FiberRefs available\")\n})\n\n// ============================================\n// 8. Putting it all together\n// ============================================\n\nconst debugSession = Effect.gen(function* () {\n yield* Effect.log(\"Starting debug session\")\n\n // Run with all debugging enabled\n yield* tracedProgram.pipe(\n withDevLogger,\n Logger.withMinimumLogLevel(LogLevel.Debug)\n )\n\n yield* debugErrors\n\n yield* Effect.log(\"Debug session complete\")\n})\n\nEffect.runPromise(debugSession)\n```\n\n### Debug Output Example\n\n```\n2024-01-15T10:30:00.000Z DEBUG [initialization] Step 1: Initialize\n2024-01-15T10:30:00.100Z DEBUG [processing] Step 2: Process\n2024-01-15T10:30:00.300Z DEBUG [finalization] Step 3: Finalize\n2024-01-15T10:30:00.350Z INFO Program complete\n```", - "antiPattern": "", - "explanation": "Effect DevTools help you:\n\n1. **See fiber state** - What's running, blocked, completed\n2. **Trace execution** - Follow the flow of effects\n3. **Debug errors** - Understand failure chains\n4. **Profile performance** - Find slow operations\n\n---", - "content": "## Guideline\n\nUse Effect's built-in debugging capabilities, logging, and fiber introspection for development.\n\n---\n\n## Rationale\n\nEffect DevTools help you:\n\n1. **See fiber state** - What's running, blocked, completed\n2. **Trace execution** - Follow the flow of effects\n3. **Debug errors** - Understand failure chains\n4. **Profile performance** - Find slow operations\n\n---\n\n## Good Example\n\n### 1. Enable Debug Mode\n\n```typescript\nimport { Effect, Logger, LogLevel, FiberRef, Cause } from \"effect\"\n\n// ============================================\n// 1. Verbose logging for development\n// ============================================\n\nconst debugProgram = Effect.gen(function* () {\n yield* Effect.logDebug(\"Starting operation\")\n\n const result = yield* someEffect.pipe(\n Effect.tap((value) => Effect.logDebug(`Got value: ${value}`))\n )\n\n yield* Effect.logDebug(\"Operation complete\")\n return result\n})\n\n// Run with debug logging enabled\nconst runWithDebug = debugProgram.pipe(\n Logger.withMinimumLogLevel(LogLevel.Debug),\n Effect.runPromise\n)\n\n// ============================================\n// 2. Fiber supervision and introspection\n// ============================================\n\nconst inspectFibers = Effect.gen(function* () {\n // Fork some fibers\n const fiber1 = yield* Effect.fork(Effect.sleep(\"1 second\"))\n const fiber2 = yield* Effect.fork(Effect.sleep(\"2 seconds\"))\n\n // Get fiber IDs\n yield* Effect.log(`Fiber 1 ID: ${fiber1.id()}`)\n yield* Effect.log(`Fiber 2 ID: ${fiber2.id()}`)\n\n // Check fiber status\n const status1 = yield* fiber1.status\n yield* Effect.log(`Fiber 1 status: ${status1._tag}`)\n})\n\n// ============================================\n// 3. Trace execution with spans\n// ============================================\n\nconst tracedProgram = Effect.gen(function* () {\n yield* Effect.log(\"=== Starting traced program ===\")\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 1: Initialize\")\n yield* Effect.sleep(\"100 millis\")\n }).pipe(Effect.withLogSpan(\"initialization\"))\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 2: Process\")\n yield* Effect.sleep(\"200 millis\")\n }).pipe(Effect.withLogSpan(\"processing\"))\n\n yield* Effect.gen(function* () {\n yield* Effect.log(\"Step 3: Finalize\")\n yield* Effect.sleep(\"50 millis\")\n }).pipe(Effect.withLogSpan(\"finalization\"))\n\n yield* Effect.log(\"=== Program complete ===\")\n})\n\n// ============================================\n// 4. Error cause inspection\n// ============================================\n\nconst debugErrors = Effect.gen(function* () {\n const failingEffect = Effect.gen(function* () {\n yield* Effect.fail(new Error(\"Inner error\"))\n }).pipe(\n Effect.flatMap(() => Effect.fail(new Error(\"Outer error\")))\n )\n\n yield* failingEffect.pipe(\n Effect.catchAllCause((cause) =>\n Effect.gen(function* () {\n yield* Effect.log(\"=== Error Cause Analysis ===\")\n yield* Effect.log(`Pretty printed:\\n${Cause.pretty(cause)}`)\n yield* Effect.log(`Is failure: ${Cause.isFailure(cause)}`)\n yield* Effect.log(`Is interrupted: ${Cause.isInterrupted(cause)}`)\n\n // Extract all failures\n const failures = Cause.failures(cause)\n yield* Effect.log(`Failures: ${JSON.stringify([...failures])}`)\n\n return \"recovered\"\n })\n )\n )\n})\n\n// ============================================\n// 5. Context inspection\n// ============================================\n\nimport { Context } from \"effect\"\n\nclass Config extends Context.Tag(\"Config\")() {}\n\nconst inspectContext = Effect.gen(function* () {\n const context = yield* Effect.context()\n\n yield* Effect.log(\"=== Context Contents ===\")\n yield* Effect.log(`Has Config: ${Context.getOption(context, Config)._tag}`)\n})\n\n// ============================================\n// 6. Custom logger for development\n// ============================================\n\nconst devLogger = Logger.make(({ logLevel, message, date, annotations, spans }) => {\n const timestamp = date.toISOString()\n const level = logLevel.label.padEnd(7)\n const spanInfo = spans.length > 0\n ? ` [${[...spans].map(([name]) => name).join(\" > \")}]`\n : \"\"\n const annotationInfo = Object.keys(annotations).length > 0\n ? ` ${JSON.stringify(Object.fromEntries(annotations))}`\n : \"\"\n\n console.log(`${timestamp} ${level}${spanInfo} ${message}${annotationInfo}`)\n})\n\nconst withDevLogger = (effect: Effect.Effect) =>\n effect.pipe(\n Effect.provide(Logger.replace(Logger.defaultLogger, devLogger))\n )\n\n// ============================================\n// 7. Runtime metrics\n// ============================================\n\nconst showRuntimeMetrics = Effect.gen(function* () {\n const runtime = yield* Effect.runtime()\n\n yield* Effect.log(\"=== Runtime Info ===\")\n // Access runtime configuration\n const fiberRefs = runtime.fiberRefs\n\n yield* Effect.log(\"FiberRefs available\")\n})\n\n// ============================================\n// 8. Putting it all together\n// ============================================\n\nconst debugSession = Effect.gen(function* () {\n yield* Effect.log(\"Starting debug session\")\n\n // Run with all debugging enabled\n yield* tracedProgram.pipe(\n withDevLogger,\n Logger.withMinimumLogLevel(LogLevel.Debug)\n )\n\n yield* debugErrors\n\n yield* Effect.log(\"Debug session complete\")\n})\n\nEffect.runPromise(debugSession)\n```\n\n### Debug Output Example\n\n```\n2024-01-15T10:30:00.000Z DEBUG [initialization] Step 1: Initialize\n2024-01-15T10:30:00.100Z DEBUG [processing] Step 2: Process\n2024-01-15T10:30:00.300Z DEBUG [finalization] Step 3: Finalize\n2024-01-15T10:30:00.350Z INFO Program complete\n```\n\n## Debugging Features\n\n| Feature | Purpose |\n|---------|---------|\n| **Log levels** | Filter by severity |\n| **Spans** | Track execution time |\n| **Annotations** | Add context to logs |\n| **Cause** | Inspect error chains |\n| **Fiber status** | Check fiber state |\n\n## Debug Techniques\n\n| Technique | When to Use |\n|-----------|-------------|\n| `Effect.log` | Track execution flow |\n| `Effect.tap` | Inspect values |\n| `withLogSpan` | Measure timing |\n| `Cause.pretty` | Understand errors |\n| `fiber.status` | Debug concurrency |\n\n## Best Practices\n\n1. **Use log levels** - Debug for dev, Info for prod\n2. **Add spans** - Find slow operations\n3. **Include context** - Know what was happening\n4. **Pretty print causes** - Understand error chains\n5. **Remove debug logs** - Before committing" - }, - { - "id": "use-gen-for-business-logic", - "title": "Use Effect.gen for Business Logic", - "description": "Use Effect.gen for business logic.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Concrete implementations for demonstration\nconst validateUser = (\n data: any\n): Effect.Effect<{ email: string; password: string }, Error, never> =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Validating user data: ${JSON.stringify(data)}`);\n\n if (!data.email || !data.password) {\n return yield* Effect.fail(new Error(\"Email and password are required\"));\n }\n\n if (data.password.length < 6) {\n return yield* Effect.fail(\n new Error(\"Password must be at least 6 characters\")\n );\n }\n\n yield* Effect.logInfo(\"✅ User data validated successfully\");\n return { email: data.email, password: data.password };\n });\n\nconst hashPassword = (pw: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Hashing password...\");\n // Simulate password hashing\n const timestamp = yield* Effect.sync(() => Date.now());\n const hashed = `hashed_${pw}_${timestamp}`;\n yield* Effect.logInfo(\"✅ Password hashed successfully\");\n return hashed;\n });\n\nconst dbCreateUser = (data: {\n email: string;\n password: string;\n}): Effect.Effect<{ id: number; email: string }, never, never> =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Creating user in database: ${data.email}`);\n // Simulate database operation\n const user = { id: Math.floor(Math.random() * 1000), email: data.email };\n yield* Effect.logInfo(`✅ User created with ID: ${user.id}`);\n return user;\n });\n\nconst createUser = (\n userData: any\n): Effect.Effect<{ id: number; email: string }, Error, never> =>\n Effect.gen(function* () {\n const validated = yield* validateUser(userData);\n const hashed = yield* hashPassword(validated.password);\n return yield* dbCreateUser({ ...validated, password: hashed });\n });\n\n// Demonstrate using Effect.gen for business logic\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Using Effect.gen for Business Logic Demo ===\");\n\n // Example 1: Successful user creation\n yield* Effect.logInfo(\"\\n1. Creating a valid user:\");\n const validUser = yield* createUser({\n email: \"paul@example.com\",\n password: \"securepassword123\",\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to create user: ${error.message}`);\n return { id: -1, email: \"error\" };\n })\n )\n );\n yield* Effect.logInfo(`Created user: ${JSON.stringify(validUser)}`);\n\n // Example 2: Invalid user data\n yield* Effect.logInfo(\"\\n2. Attempting to create user with invalid data:\");\n const invalidUser = yield* createUser({\n email: \"invalid@example.com\",\n password: \"123\", // Too short\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to create user: ${error.message}`);\n return { id: -1, email: \"error\" };\n })\n )\n );\n yield* Effect.logInfo(`Result: ${JSON.stringify(invalidUser)}`);\n\n yield* Effect.logInfo(\"\\n✅ Business logic demonstration completed!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.gen` allows you to express business logic in a clear, sequential style,\nimproving maintainability.", - "antiPattern": "Using long chains of `.andThen` or `.flatMap` for multi-step business logic.\nThis is harder to read and pass state between steps.", - "explanation": "Generators provide a syntax that closely resembles standard synchronous code\n(`async/await`), making complex workflows significantly easier to read, write,\nand debug.", - "content": "# Use Effect.gen for Business Logic\n\n## Guideline\n\nUse `Effect.gen` to write your core business logic, especially when it involves\nmultiple sequential steps or conditional branching.\n\n## Rationale\n\nGenerators provide a syntax that closely resembles standard synchronous code\n(`async/await`), making complex workflows significantly easier to read, write,\nand debug.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Concrete implementations for demonstration\nconst validateUser = (\n data: any\n): Effect.Effect<{ email: string; password: string }, Error, never> =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Validating user data: ${JSON.stringify(data)}`);\n\n if (!data.email || !data.password) {\n return yield* Effect.fail(new Error(\"Email and password are required\"));\n }\n\n if (data.password.length < 6) {\n return yield* Effect.fail(\n new Error(\"Password must be at least 6 characters\")\n );\n }\n\n yield* Effect.logInfo(\"✅ User data validated successfully\");\n return { email: data.email, password: data.password };\n });\n\nconst hashPassword = (pw: string): Effect.Effect =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Hashing password...\");\n // Simulate password hashing\n const timestamp = yield* Effect.sync(() => Date.now());\n const hashed = `hashed_${pw}_${timestamp}`;\n yield* Effect.logInfo(\"✅ Password hashed successfully\");\n return hashed;\n });\n\nconst dbCreateUser = (data: {\n email: string;\n password: string;\n}): Effect.Effect<{ id: number; email: string }, never, never> =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Creating user in database: ${data.email}`);\n // Simulate database operation\n const user = { id: Math.floor(Math.random() * 1000), email: data.email };\n yield* Effect.logInfo(`✅ User created with ID: ${user.id}`);\n return user;\n });\n\nconst createUser = (\n userData: any\n): Effect.Effect<{ id: number; email: string }, Error, never> =>\n Effect.gen(function* () {\n const validated = yield* validateUser(userData);\n const hashed = yield* hashPassword(validated.password);\n return yield* dbCreateUser({ ...validated, password: hashed });\n });\n\n// Demonstrate using Effect.gen for business logic\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Using Effect.gen for Business Logic Demo ===\");\n\n // Example 1: Successful user creation\n yield* Effect.logInfo(\"\\n1. Creating a valid user:\");\n const validUser = yield* createUser({\n email: \"paul@example.com\",\n password: \"securepassword123\",\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to create user: ${error.message}`);\n return { id: -1, email: \"error\" };\n })\n )\n );\n yield* Effect.logInfo(`Created user: ${JSON.stringify(validUser)}`);\n\n // Example 2: Invalid user data\n yield* Effect.logInfo(\"\\n2. Attempting to create user with invalid data:\");\n const invalidUser = yield* createUser({\n email: \"invalid@example.com\",\n password: \"123\", // Too short\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to create user: ${error.message}`);\n return { id: -1, email: \"error\" };\n })\n )\n );\n yield* Effect.logInfo(`Result: ${JSON.stringify(invalidUser)}`);\n\n yield* Effect.logInfo(\"\\n✅ Business logic demonstration completed!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.gen` allows you to express business logic in a clear, sequential style,\nimproving maintainability.\n\n## Anti-Pattern\n\nUsing long chains of `.andThen` or `.flatMap` for multi-step business logic.\nThis is harder to read and pass state between steps." - }, - { - "id": "use-default-layer-for-tests", - "title": "Use the Auto-Generated .Default Layer in Tests", - "description": "Use the auto-generated .Default layer in tests.", - "skillLevel": "intermediate", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Define MyService using Effect.Service pattern\nclass MyService extends Effect.Service()(\"MyService\", {\n sync: () => ({\n doSomething: () =>\n Effect.succeed(\"done\").pipe(\n Effect.tap(() => Effect.log(\"MyService did something!\"))\n ),\n }),\n}) {}\n\n// Create a program that uses MyService\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Getting MyService...\");\n const service = yield* MyService;\n\n yield* Effect.log(\"Calling doSomething()...\");\n const result = yield* service.doSomething();\n\n yield* Effect.log(`Result: ${result}`);\n});\n\n// Run the program with default service implementation\nEffect.runPromise(Effect.provide(program, MyService.Default));\n```\n\n**Explanation:** \nThis approach ensures your tests are idiomatic, maintainable, and take full advantage of Effect's dependency injection system.", - "antiPattern": "Do not create manual layers for your service in tests (`Layer.succeed(...)`) or try to provide the service class directly. This bypasses the intended dependency injection mechanism.", - "explanation": "The `.Default` layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust.", - "content": "# Use the Auto-Generated .Default Layer in Tests\n\n## Guideline\n\nIn your tests, provide service dependencies using the static `.Default` property that `Effect.Service` automatically attaches to your service class.\n\n## Rationale\n\nThe `.Default` layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define MyService using Effect.Service pattern\nclass MyService extends Effect.Service()(\"MyService\", {\n sync: () => ({\n doSomething: () =>\n Effect.succeed(\"done\").pipe(\n Effect.tap(() => Effect.log(\"MyService did something!\"))\n ),\n }),\n}) {}\n\n// Create a program that uses MyService\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"Getting MyService...\");\n const service = yield* MyService;\n\n yield* Effect.log(\"Calling doSomething()...\");\n const result = yield* service.doSomething();\n\n yield* Effect.log(`Result: ${result}`);\n});\n\n// Run the program with default service implementation\nEffect.runPromise(Effect.provide(program, MyService.Default));\n```\n\n**Explanation:** \nThis approach ensures your tests are idiomatic, maintainable, and take full advantage of Effect's dependency injection system.\n\n## Anti-Pattern\n\nDo not create manual layers for your service in tests (`Layer.succeed(...)`) or try to provide the service class directly. This bypasses the intended dependency injection mechanism." - }, - { - "id": "validate-request-body", - "title": "Validate Request Body", - "description": "Use Http.request.schemaBodyJson with a Schema to automatically parse and validate request bodies.", - "skillLevel": "intermediate", - "useCases": [ - "building-apis" - ], - "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.\n\n```typescript\nimport { Duration, Effect } from \"effect\";\nimport * as S from \"effect/Schema\";\nimport { createServer, IncomingMessage, ServerResponse } from \"http\";\n\n// Define user schema\nconst UserSchema = S.Struct({\n name: S.String,\n email: S.String.pipe(S.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)),\n});\ntype User = S.Schema.Type;\n\n// Define user service interface\ninterface UserServiceInterface {\n readonly validateUser: (data: unknown) => Effect.Effect;\n}\n\n// Define user service\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n validateUser: (data: unknown) => S.decodeUnknown(UserSchema)(data),\n }),\n}) {}\n\n// Define HTTP server service interface\ninterface HttpServerInterface {\n readonly handleRequest: (\n request: IncomingMessage,\n response: ServerResponse\n ) => Effect.Effect;\n readonly start: () => Effect.Effect;\n}\n\n// Define HTTP server service\nclass HttpServer extends Effect.Service()(\"HttpServer\", {\n // Define effect-based implementation that uses dependencies\n effect: Effect.gen(function* () {\n const userService = yield* UserService;\n\n return {\n handleRequest: (request: IncomingMessage, response: ServerResponse) =>\n Effect.gen(function* () {\n // Only handle POST /users\n if (request.method !== \"POST\" || request.url !== \"/users\") {\n response.writeHead(404, { \"Content-Type\": \"application/json\" });\n response.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n try {\n // Read request body\n const body = yield* Effect.async((resume) => {\n let data = \"\";\n request.on(\"data\", (chunk) => {\n data += chunk;\n });\n request.on(\"end\", () => {\n try {\n resume(Effect.succeed(JSON.parse(data)));\n } catch (e) {\n resume(\n Effect.fail(e instanceof Error ? e : new Error(String(e)))\n );\n }\n });\n request.on(\"error\", (e) =>\n resume(\n Effect.fail(e instanceof Error ? e : new Error(String(e)))\n )\n );\n });\n\n // Validate body against schema\n const user = yield* userService.validateUser(body);\n\n response.writeHead(200, { \"Content-Type\": \"application/json\" });\n response.end(\n JSON.stringify({\n message: `Successfully created user: ${user.name}`,\n })\n );\n } catch (error) {\n response.writeHead(400, { \"Content-Type\": \"application/json\" });\n response.end(JSON.stringify({ error: String(error) }));\n }\n }),\n\n start: function (this: HttpServer) {\n const self = this;\n return Effect.gen(function* () {\n // Create HTTP server\n const server = createServer((req, res) =>\n Effect.runFork(self.handleRequest(req, res))\n );\n\n // Add cleanup finalizer\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shut down\");\n })\n );\n\n // Start server\n yield* Effect.async((resume) => {\n server.on(\"error\", (error) => resume(Effect.fail(error)));\n server.listen(3456, () => {\n Effect.runFork(\n Effect.logInfo(\"Server running at http://localhost:3456/\")\n );\n resume(Effect.succeed(void 0));\n });\n });\n\n // Run for demonstration period\n yield* Effect.sleep(Duration.seconds(3));\n yield* Effect.logInfo(\"Demo completed - shutting down server\");\n });\n },\n };\n }),\n // Specify dependencies\n dependencies: [UserService.Default],\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const server = yield* HttpServer;\n\n yield* Effect.logInfo(\"Starting HTTP server...\");\n\n yield* server.start().pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Server error: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n );\n}).pipe(\n Effect.scoped // Ensure server is cleaned up\n);\n\n// Run the server\nEffect.runFork(Effect.provide(program, HttpServer.Default));\n\n/*\nTo test:\n- POST http://localhost:3456/users with body {\"name\": \"Paul\", \"email\": \"paul@effect.com\"}\n -> Returns 200 OK with message \"Successfully created user: Paul\"\n\n- POST http://localhost:3456/users with body {\"name\": \"Paul\"}\n -> Returns 400 Bad Request with error message about missing email field\n*/\n```", - "antiPattern": "The anti-pattern is to manually parse the JSON and then write imperative validation checks. This approach is verbose, error-prone, and not type-safe.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst createUserRoute = Http.router.post(\n \"/users\",\n Http.request.json.pipe(\n // Http.request.json returns Effect\n Effect.flatMap((body) => {\n // Manually check the type and properties of the body.\n if (\n typeof body === \"object\" &&\n body !== null &&\n \"name\" in body &&\n typeof body.name === \"string\" &&\n \"email\" in body &&\n typeof body.email === \"string\"\n ) {\n // The type is still not safely inferred here without casting.\n return Http.response.text(`Successfully created user: ${body.name}`);\n } else {\n // Manually create and return a generic error response.\n return Http.response.text(\"Invalid request body\", { status: 400 });\n }\n })\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(createUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual code is significantly worse. It's hard to read, easy to get wrong, and loses all static type information from the parsed body. Crucially, it forces you to reinvent the wheel for error reporting, which will likely be less detailed and consistent than the automatic responses provided by the platform.", - "explanation": "Accepting user-provided data is one of the most critical and sensitive parts of an API. You must never trust incoming data. The `Http` module's integration with `Schema` provides a robust, declarative solution for this.\n\nUsing `Http.request.schemaBodyJson` offers several major advantages:\n\n1. **Automatic Validation and Error Handling**: If the incoming body does not match the schema, the server automatically rejects the request with a `400 Bad Request` status and a detailed JSON response explaining the validation errors. You don't have to write any of this boilerplate logic.\n2. **Type Safety**: If the validation succeeds, the value produced by the `Effect` is fully typed according to your `Schema`. This eliminates `any` types and brings static analysis benefits to your request handlers.\n3. **Declarative and Clean**: The validation rules are defined once in the `Schema` and then simply applied. This separates the validation logic from your business logic, keeping handlers clean and focused on their core task.\n4. **Security**: It acts as a security gateway, ensuring that malformed or unexpected data structures never reach your application's core logic.\n\n---", - "content": "## Guideline\n\nTo process an incoming request body, use `Http.request.schemaBodyJson(YourSchema)` to parse the JSON and validate its structure in a single, type-safe step.\n\n---\n\n## Rationale\n\nAccepting user-provided data is one of the most critical and sensitive parts of an API. You must never trust incoming data. The `Http` module's integration with `Schema` provides a robust, declarative solution for this.\n\nUsing `Http.request.schemaBodyJson` offers several major advantages:\n\n1. **Automatic Validation and Error Handling**: If the incoming body does not match the schema, the server automatically rejects the request with a `400 Bad Request` status and a detailed JSON response explaining the validation errors. You don't have to write any of this boilerplate logic.\n2. **Type Safety**: If the validation succeeds, the value produced by the `Effect` is fully typed according to your `Schema`. This eliminates `any` types and brings static analysis benefits to your request handlers.\n3. **Declarative and Clean**: The validation rules are defined once in the `Schema` and then simply applied. This separates the validation logic from your business logic, keeping handlers clean and focused on their core task.\n4. **Security**: It acts as a security gateway, ensuring that malformed or unexpected data structures never reach your application's core logic.\n\n---\n\n## Good Example\n\nThis 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.\n\n```typescript\nimport { Duration, Effect } from \"effect\";\nimport * as S from \"effect/Schema\";\nimport { createServer, IncomingMessage, ServerResponse } from \"http\";\n\n// Define user schema\nconst UserSchema = S.Struct({\n name: S.String,\n email: S.String.pipe(S.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)),\n});\ntype User = S.Schema.Type;\n\n// Define user service interface\ninterface UserServiceInterface {\n readonly validateUser: (data: unknown) => Effect.Effect;\n}\n\n// Define user service\nclass UserService extends Effect.Service()(\"UserService\", {\n sync: () => ({\n validateUser: (data: unknown) => S.decodeUnknown(UserSchema)(data),\n }),\n}) {}\n\n// Define HTTP server service interface\ninterface HttpServerInterface {\n readonly handleRequest: (\n request: IncomingMessage,\n response: ServerResponse\n ) => Effect.Effect;\n readonly start: () => Effect.Effect;\n}\n\n// Define HTTP server service\nclass HttpServer extends Effect.Service()(\"HttpServer\", {\n // Define effect-based implementation that uses dependencies\n effect: Effect.gen(function* () {\n const userService = yield* UserService;\n\n return {\n handleRequest: (request: IncomingMessage, response: ServerResponse) =>\n Effect.gen(function* () {\n // Only handle POST /users\n if (request.method !== \"POST\" || request.url !== \"/users\") {\n response.writeHead(404, { \"Content-Type\": \"application/json\" });\n response.end(JSON.stringify({ error: \"Not Found\" }));\n return;\n }\n\n try {\n // Read request body\n const body = yield* Effect.async((resume) => {\n let data = \"\";\n request.on(\"data\", (chunk) => {\n data += chunk;\n });\n request.on(\"end\", () => {\n try {\n resume(Effect.succeed(JSON.parse(data)));\n } catch (e) {\n resume(\n Effect.fail(e instanceof Error ? e : new Error(String(e)))\n );\n }\n });\n request.on(\"error\", (e) =>\n resume(\n Effect.fail(e instanceof Error ? e : new Error(String(e)))\n )\n );\n });\n\n // Validate body against schema\n const user = yield* userService.validateUser(body);\n\n response.writeHead(200, { \"Content-Type\": \"application/json\" });\n response.end(\n JSON.stringify({\n message: `Successfully created user: ${user.name}`,\n })\n );\n } catch (error) {\n response.writeHead(400, { \"Content-Type\": \"application/json\" });\n response.end(JSON.stringify({ error: String(error) }));\n }\n }),\n\n start: function (this: HttpServer) {\n const self = this;\n return Effect.gen(function* () {\n // Create HTTP server\n const server = createServer((req, res) =>\n Effect.runFork(self.handleRequest(req, res))\n );\n\n // Add cleanup finalizer\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n yield* Effect.sync(() => server.close());\n yield* Effect.logInfo(\"Server shut down\");\n })\n );\n\n // Start server\n yield* Effect.async((resume) => {\n server.on(\"error\", (error) => resume(Effect.fail(error)));\n server.listen(3456, () => {\n Effect.runFork(\n Effect.logInfo(\"Server running at http://localhost:3456/\")\n );\n resume(Effect.succeed(void 0));\n });\n });\n\n // Run for demonstration period\n yield* Effect.sleep(Duration.seconds(3));\n yield* Effect.logInfo(\"Demo completed - shutting down server\");\n });\n },\n };\n }),\n // Specify dependencies\n dependencies: [UserService.Default],\n}) {}\n\n// Create program with proper error handling\nconst program = Effect.gen(function* () {\n const server = yield* HttpServer;\n\n yield* Effect.logInfo(\"Starting HTTP server...\");\n\n yield* server.start().pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Server error: ${error}`);\n return yield* Effect.fail(error);\n })\n )\n );\n}).pipe(\n Effect.scoped // Ensure server is cleaned up\n);\n\n// Run the server\nEffect.runFork(Effect.provide(program, HttpServer.Default));\n\n/*\nTo test:\n- POST http://localhost:3456/users with body {\"name\": \"Paul\", \"email\": \"paul@effect.com\"}\n -> Returns 200 OK with message \"Successfully created user: Paul\"\n\n- POST http://localhost:3456/users with body {\"name\": \"Paul\"}\n -> Returns 400 Bad Request with error message about missing email field\n*/\n```\n\n## Anti-Pattern\n\nThe anti-pattern is to manually parse the JSON and then write imperative validation checks. This approach is verbose, error-prone, and not type-safe.\n\n```typescript\nimport { Effect } from \"effect\";\nimport { Http, NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n\nconst createUserRoute = Http.router.post(\n \"/users\",\n Http.request.json.pipe(\n // Http.request.json returns Effect\n Effect.flatMap((body) => {\n // Manually check the type and properties of the body.\n if (\n typeof body === \"object\" &&\n body !== null &&\n \"name\" in body &&\n typeof body.name === \"string\" &&\n \"email\" in body &&\n typeof body.email === \"string\"\n ) {\n // The type is still not safely inferred here without casting.\n return Http.response.text(`Successfully created user: ${body.name}`);\n } else {\n // Manually create and return a generic error response.\n return Http.response.text(\"Invalid request body\", { status: 400 });\n }\n })\n )\n);\n\nconst app = Http.router.empty.pipe(Http.router.addRoute(createUserRoute));\n\nconst program = Http.server\n .serve(app)\n .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));\n\nNodeRuntime.runMain(program);\n```\n\nThis manual code is significantly worse. It's hard to read, easy to get wrong, and loses all static type information from the parsed body. Crucially, it forces you to reinvent the wheel for error reporting, which will likely be less detailed and consistent than the automatic responses provided by the platform." - }, - { - "id": "brand-validate-parse", - "title": "Validating and Parsing Branded Types", - "description": "Combine Schema and Brand to validate and parse branded types, guaranteeing only valid domain values are created at runtime.", - "skillLevel": "intermediate", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Brand, Effect, Schema } from \"effect\";\n\n// Define a branded type for Email\ntype Email = string & Brand.Brand<\"Email\">;\n\n// Create a Schema for Email validation\nconst EmailSchema = Schema.String.pipe(\n Schema.pattern(/^[^@]+@[^@]+\\.[^@]+$/), // Simple email regex\n Schema.brand(\"Email\" as const) // Attach the brand\n);\n\n// Parse and validate an email at runtime\nconst parseEmail = (input: string) =>\n Effect.try({\n try: () => Schema.decodeSync(EmailSchema)(input),\n catch: (err) => `Invalid email: ${String(err)}`,\n });\n\n// Usage\nparseEmail(\"user@example.com\").pipe(\n Effect.match({\n onSuccess: (email) => console.log(\"Valid email:\", email),\n onFailure: (err) => console.error(err),\n })\n);\n```\n\n**Explanation:**\n\n- `Schema` is used to define validation logic for the branded type.\n- `Brand.schema()` attaches the brand to the schema, so only validated values can be constructed as `Email`.\n- This pattern ensures both compile-time and runtime safety.", - "antiPattern": "Branding values without runtime validation, or accepting unvalidated user input as branded types, which can lead to invalid domain values and runtime bugs.", - "explanation": "While branding types at the type level prevents accidental misuse, runtime validation is needed to ensure only valid values are constructed from user input, APIs, or external sources.", - "content": "# Validating and Parsing Branded Types\n\n## Guideline\n\nUse `Schema` in combination with `Brand` to validate and parse branded types at runtime. \nThis ensures that only values passing your validation logic can be constructed as branded types, making your domain models robust and type-safe.\n\n## Rationale\n\nWhile branding types at the type level prevents accidental misuse, runtime validation is needed to ensure only valid values are constructed from user input, APIs, or external sources.\n\n## Good Example\n\n```typescript\nimport { Brand, Effect, Schema } from \"effect\";\n\n// Define a branded type for Email\ntype Email = string & Brand.Brand<\"Email\">;\n\n// Create a Schema for Email validation\nconst EmailSchema = Schema.String.pipe(\n Schema.pattern(/^[^@]+@[^@]+\\.[^@]+$/), // Simple email regex\n Schema.brand(\"Email\" as const) // Attach the brand\n);\n\n// Parse and validate an email at runtime\nconst parseEmail = (input: string) =>\n Effect.try({\n try: () => Schema.decodeSync(EmailSchema)(input),\n catch: (err) => `Invalid email: ${String(err)}`,\n });\n\n// Usage\nparseEmail(\"user@example.com\").pipe(\n Effect.match({\n onSuccess: (email) => console.log(\"Valid email:\", email),\n onFailure: (err) => console.error(err),\n })\n);\n```\n\n**Explanation:**\n\n- `Schema` is used to define validation logic for the branded type.\n- `Brand.schema()` attaches the brand to the schema, so only validated values can be constructed as `Email`.\n- This pattern ensures both compile-time and runtime safety.\n\n## Anti-Pattern\n\nBranding values without runtime validation, or accepting unvalidated user input as branded types, which can lead to invalid domain values and runtime bugs." - }, - { - "id": "getting-started-effect-vs-promise", - "title": "Why Effect? Comparing Effect to Promise", - "description": "Understand why Effect is better than raw Promises.", - "skillLevel": "beginner", - "useCases": [ - "getting-started" - ], - "example": "", - "antiPattern": "", - "explanation": "", - "content": "# Why Effect? Comparing Effect to Promise\n\n## Guideline\n\nEffect solves three problems that Promises don't:\n1. **Errors are typed** - You know exactly what can go wrong\n2. **Dependencies are tracked** - You know what services are needed\n3. **Effects are lazy** - Nothing runs until you say so\n\n## The Problem with Promises\n\n```typescript\n// With Promises, errors are invisible in the type system\nasync function fetchUser(id: string): Promise {\n const response = await fetch(`/api/users/${id}`);\n if (!response.ok) throw new Error(\"Failed to fetch\"); // What errors can happen?\n return response.json();\n}\n\n// The type says Promise, but it might throw anything!\n```\n\n## The Effect Solution\n\n```typescript\nimport { Effect } from \"effect\";\n\nclass FetchError {\n readonly _tag = \"FetchError\";\n constructor(readonly message: string) {}\n}\n\nclass ParseError {\n readonly _tag = \"ParseError\";\n constructor(readonly message: string) {}\n}\n\n// The type tells you EXACTLY what can go wrong\nconst fetchUser = (id: string): Effect.Effect =>\n Effect.gen(function* () {\n const response = yield* Effect.tryPromise({\n try: () => fetch(`/api/users/${id}`),\n catch: () => new FetchError(\"Network request failed\"),\n });\n\n if (!response.ok) {\n return yield* Effect.fail(new FetchError(`HTTP ${response.status}`));\n }\n\n const json = yield* Effect.tryPromise({\n try: () => response.json(),\n catch: () => new ParseError(\"Invalid JSON\"),\n });\n\n return json as User;\n });\n```\n\n## Side-by-Side Comparison\n\n| Feature | Promise | Effect |\n|---------|---------|--------|\n| Error types | `any` (unknown) | Explicit in type signature |\n| Execution | Immediate (eager) | Lazy (runs when you say) |\n| Dependencies | Global/implicit | Explicit in type signature |\n| Composition | `.then()` chains | Powerful combinators |\n| Cancellation | AbortController (manual) | Built-in fiber interruption |\n| Retry/Timeout | Manual implementation | One-liner with Schedule |\n\n## Key Insight\n\nWith Promises:\n```typescript\n// You have to read the implementation to know what might fail\nasync function doSomething(): Promise { ... }\n```\n\nWith Effect:\n```typescript\n// The type tells you everything\nfunction doSomething(): Effect { ... }\n// ^success ^errors ^dependencies\n```\n\n## When to Use Effect\n\n- ✅ Complex business logic with multiple error types\n- ✅ Applications needing dependency injection\n- ✅ Code that needs to be testable\n- ✅ Operations needing retry, timeout, or concurrency control\n\n## When Promises Are Fine\n\n- ✅ Simple scripts\n- ✅ One-off async operations\n- ✅ When error handling is straightforward" - }, - { - "id": "data-bigdecimal", - "title": "Work with Arbitrary-Precision Numbers using BigDecimal", - "description": "Use BigDecimal to represent and compute with decimal numbers that require arbitrary precision, such as in finance or scientific domains.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { BigDecimal } from \"effect\";\n\n// Create BigDecimal values\nconst a = BigDecimal.fromNumber(0.1);\nconst b = BigDecimal.fromNumber(0.2);\n\n// Add, subtract, multiply, divide\nconst sum = BigDecimal.sum(a, b); // BigDecimal(0.3)\nconst product = BigDecimal.multiply(a, b); // BigDecimal(0.02)\n\n// Compare values\nconst isEqual = BigDecimal.equals(sum, BigDecimal.fromNumber(0.3)); // true\n\n// Convert to string or number\nconst asString = BigDecimal.format(BigDecimal.normalize(sum)); // \"0.3\"\nconst asNumber = BigDecimal.unsafeToNumber(sum); // 0.3\n```\n\n**Explanation:**\n\n- `BigDecimal` is immutable and supports precise decimal arithmetic.\n- Use it for domains where rounding errors are unacceptable (e.g., finance, billing, scientific data).\n- Avoids the pitfalls of floating-point math in JavaScript.", - "antiPattern": "Using JavaScript's native `number` type for financial or scientific calculations, which can lead to rounding errors and loss of precision.", - "explanation": "JavaScript's `number` type is a floating-point double, which can introduce subtle bugs in calculations that require exact decimal representation. \n`BigDecimal` provides precise, immutable arithmetic for critical domains.", - "content": "# Work with Arbitrary-Precision Numbers using `BigDecimal`\n\n## Guideline\n\nUse the `BigDecimal` data type for decimal numbers that require arbitrary precision, such as financial or scientific calculations. \nThis avoids rounding errors and loss of precision that can occur with JavaScript's native `number` type.\n\n## Rationale\n\nJavaScript's `number` type is a floating-point double, which can introduce subtle bugs in calculations that require exact decimal representation. \n`BigDecimal` provides precise, immutable arithmetic for critical domains.\n\n## Good Example\n\n```typescript\nimport { BigDecimal } from \"effect\";\n\n// Create BigDecimal values\nconst a = BigDecimal.fromNumber(0.1);\nconst b = BigDecimal.fromNumber(0.2);\n\n// Add, subtract, multiply, divide\nconst sum = BigDecimal.sum(a, b); // BigDecimal(0.3)\nconst product = BigDecimal.multiply(a, b); // BigDecimal(0.02)\n\n// Compare values\nconst isEqual = BigDecimal.equals(sum, BigDecimal.fromNumber(0.3)); // true\n\n// Convert to string or number\nconst asString = BigDecimal.format(BigDecimal.normalize(sum)); // \"0.3\"\nconst asNumber = BigDecimal.unsafeToNumber(sum); // 0.3\n```\n\n**Explanation:**\n\n- `BigDecimal` is immutable and supports precise decimal arithmetic.\n- Use it for domains where rounding errors are unacceptable (e.g., finance, billing, scientific data).\n- Avoids the pitfalls of floating-point math in JavaScript.\n\n## Anti-Pattern\n\nUsing JavaScript's native `number` type for financial or scientific calculations, which can lead to rounding errors and loss of precision." - }, - { - "id": "data-datetime", - "title": "Work with Dates and Times using DateTime", - "description": "Use DateTime to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { DateTime } from \"effect\";\n\n// Create a DateTime for the current instant (returns an Effect)\nimport { Effect } from \"effect\";\n\nconst program = Effect.gen(function* () {\n const now = yield* DateTime.now; // DateTime.Utc\n\n // Parse from ISO string\n const parsed = DateTime.unsafeMakeZoned(\"2024-07-19T12:34:56Z\"); // DateTime.Zoned\n\n // Add or subtract durations\n const inOneHour = DateTime.add(now, { hours: 1 });\n const oneHourAgo = DateTime.subtract(now, { hours: 1 });\n\n // Format as ISO string\n const iso = DateTime.formatIso(now); // e.g., \"2024-07-19T23:33:19.000Z\"\n\n // Compare DateTimes\n const isBefore = DateTime.lessThan(oneHourAgo, now); // true\n\n return { now, inOneHour, oneHourAgo, iso, isBefore };\n});\n```\n\n**Explanation:**\n\n- `DateTime` is immutable and time-zone-aware.\n- Supports parsing, formatting, arithmetic, and comparison.\n- Use for all date/time logic to avoid bugs with native `Date`.", - "antiPattern": "Using JavaScript's mutable `Date` for time calculations, or ignoring time zones, which can lead to subtle and hard-to-debug errors.", - "explanation": "JavaScript's native `Date` is mutable, not time-zone-aware, and can be error-prone. \n`DateTime` provides an immutable, functional alternative with explicit time zone handling and robust APIs for time arithmetic.", - "content": "# Work with Dates and Times using `DateTime`\n\n## Guideline\n\nUse the `DateTime` data type to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way. \nThis enables safe, precise, and reliable time calculations in your applications.\n\n## Rationale\n\nJavaScript's native `Date` is mutable, not time-zone-aware, and can be error-prone. \n`DateTime` provides an immutable, functional alternative with explicit time zone handling and robust APIs for time arithmetic.\n\n## Good Example\n\n```typescript\nimport { DateTime } from \"effect\";\n\n// Create a DateTime for the current instant (returns an Effect)\nimport { Effect } from \"effect\";\n\nconst program = Effect.gen(function* () {\n const now = yield* DateTime.now; // DateTime.Utc\n\n // Parse from ISO string\n const parsed = DateTime.unsafeMakeZoned(\"2024-07-19T12:34:56Z\"); // DateTime.Zoned\n\n // Add or subtract durations\n const inOneHour = DateTime.add(now, { hours: 1 });\n const oneHourAgo = DateTime.subtract(now, { hours: 1 });\n\n // Format as ISO string\n const iso = DateTime.formatIso(now); // e.g., \"2024-07-19T23:33:19.000Z\"\n\n // Compare DateTimes\n const isBefore = DateTime.lessThan(oneHourAgo, now); // true\n\n return { now, inOneHour, oneHourAgo, iso, isBefore };\n});\n```\n\n**Explanation:**\n\n- `DateTime` is immutable and time-zone-aware.\n- Supports parsing, formatting, arithmetic, and comparison.\n- Use for all date/time logic to avoid bugs with native `Date`.\n\n## Anti-Pattern\n\nUsing JavaScript's mutable `Date` for time calculations, or ignoring time zones, which can lead to subtle and hard-to-debug errors." - }, - { - "id": "data-hashset", - "title": "Work with Immutable Sets using HashSet", - "description": "Use HashSet to represent sets of unique values with efficient, immutable operations for membership, union, intersection, and difference.", - "skillLevel": "intermediate", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { HashSet } from \"effect\";\n\n// Create a HashSet from an array\nconst setA = HashSet.fromIterable([1, 2, 3]);\nconst setB = HashSet.fromIterable([3, 4, 5]);\n\n// Membership check\nconst hasTwo = HashSet.has(setA, 2); // true\n\n// Union, intersection, difference\nconst union = HashSet.union(setA, setB); // HashSet {1, 2, 3, 4, 5}\nconst intersection = HashSet.intersection(setA, setB); // HashSet {3}\nconst difference = HashSet.difference(setA, setB); // HashSet {1, 2}\n\n// Add and remove elements\nconst withSix = HashSet.add(setA, 6); // HashSet {1, 2, 3, 6}\nconst withoutOne = HashSet.remove(setA, 1); // HashSet {2, 3}\n```\n\n**Explanation:**\n\n- `HashSet` is immutable and supports efficient set operations.\n- Use it for membership checks, set algebra, and modeling unique collections.\n- Safe for concurrent and functional workflows.", - "antiPattern": "Using mutable JavaScript `Set` for shared or concurrent data, or for set operations in functional code, which can lead to bugs and unpredictable behavior.", - "explanation": "`HashSet` provides high-performance, immutable set operations that are safe for concurrent and functional programming. \nIt avoids the pitfalls of mutable JavaScript `Set` and is optimized for use in Effect workflows.", - "content": "# Work with Immutable Sets using `HashSet`\n\n## Guideline\n\nUse the `HashSet` data type to represent sets of unique values with efficient, immutable operations. \n`HashSet` is ideal for membership checks, set algebra, and modeling collections where uniqueness matters.\n\n## Rationale\n\n`HashSet` provides high-performance, immutable set operations that are safe for concurrent and functional programming. \nIt avoids the pitfalls of mutable JavaScript `Set` and is optimized for use in Effect workflows.\n\n## Good Example\n\n```typescript\nimport { HashSet } from \"effect\";\n\n// Create a HashSet from an array\nconst setA = HashSet.fromIterable([1, 2, 3]);\nconst setB = HashSet.fromIterable([3, 4, 5]);\n\n// Membership check\nconst hasTwo = HashSet.has(setA, 2); // true\n\n// Union, intersection, difference\nconst union = HashSet.union(setA, setB); // HashSet {1, 2, 3, 4, 5}\nconst intersection = HashSet.intersection(setA, setB); // HashSet {3}\nconst difference = HashSet.difference(setA, setB); // HashSet {1, 2}\n\n// Add and remove elements\nconst withSix = HashSet.add(setA, 6); // HashSet {1, 2, 3, 6}\nconst withoutOne = HashSet.remove(setA, 1); // HashSet {2, 3}\n```\n\n**Explanation:**\n\n- `HashSet` is immutable and supports efficient set operations.\n- Use it for membership checks, set algebra, and modeling unique collections.\n- Safe for concurrent and functional workflows.\n\n## Anti-Pattern\n\nUsing mutable JavaScript `Set` for shared or concurrent data, or for set operations in functional code, which can lead to bugs and unpredictable behavior." - }, - { - "id": "data-array", - "title": "Working with Immutable Arrays using Data.array", - "description": "Use Data.array to define arrays whose equality is based on their contents, enabling safe, predictable comparisons and functional operations.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal arrays\nconst arr1 = Data.array([1, 2, 3]);\nconst arr2 = Data.array([1, 2, 3]);\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(arr1, arr2); // true\n\n// Use arrays as keys in a HashSet or Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(arr1);\nconsole.log(HashSet.has(set, arr2)); // true\n\n// Functional operations (map, filter, etc.)\nconst doubled = arr1.map((n) => n * 2); // Data.array([2, 4, 6])\n```\n\n**Explanation:**\n\n- `Data.array` creates immutable arrays with value-based equality.\n- Useful for modeling ordered collections in a safe, functional way.\n- Supports all standard array operations, but with immutability and structural equality.", - "antiPattern": "Using plain JavaScript arrays for value-based logic, as keys in sets/maps, or in concurrent code, which can lead to bugs due to mutability and reference-based comparison.", - "explanation": "JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic and concurrent code. \n`Data.array` provides immutable arrays with structural equality, making them ideal for functional programming and safe domain modeling.", - "content": "# Working with Immutable Arrays using `Data.array`\n\n## Guideline\n\nUse `Data.array` to create immutable, type-safe arrays that support value-based equality and safe functional operations. \nThis is useful for modeling ordered collections where immutability and structural equality are important.\n\n## Rationale\n\nJavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic and concurrent code. \n`Data.array` provides immutable arrays with structural equality, making them ideal for functional programming and safe domain modeling.\n\n## Good Example\n\n```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal arrays\nconst arr1 = Data.array([1, 2, 3]);\nconst arr2 = Data.array([1, 2, 3]);\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(arr1, arr2); // true\n\n// Use arrays as keys in a HashSet or Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(arr1);\nconsole.log(HashSet.has(set, arr2)); // true\n\n// Functional operations (map, filter, etc.)\nconst doubled = arr1.map((n) => n * 2); // Data.array([2, 4, 6])\n```\n\n**Explanation:**\n\n- `Data.array` creates immutable arrays with value-based equality.\n- Useful for modeling ordered collections in a safe, functional way.\n- Supports all standard array operations, but with immutability and structural equality.\n\n## Anti-Pattern\n\nUsing plain JavaScript arrays for value-based logic, as keys in sets/maps, or in concurrent code, which can lead to bugs due to mutability and reference-based comparison." - }, - { - "id": "data-tuple", - "title": "Working with Tuples using Data.tuple", - "description": "Use Data.tuple to define tuples whose equality is based on their contents, enabling safe and predictable comparisons and pattern matching.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal tuples\nconst t1 = Data.tuple(1, \"Alice\");\nconst t2 = Data.tuple(1, \"Alice\");\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(t1, t2); // true\n\n// Use tuples as keys in a HashSet or Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(t1);\nconsole.log(HashSet.has(set, t2)); // true\n\n// Pattern matching on tuples\nconst [id, name] = t1; // id: number, name: string\n```\n\n**Explanation:**\n\n- `Data.tuple` creates immutable tuples with value-based equality.\n- Useful for modeling pairs, coordinates, or any fixed-size, heterogeneous data.\n- Supports safe pattern matching and collection operations.", - "antiPattern": "Using plain arrays for value-based logic or as keys in sets/maps, which compares by reference and can lead to incorrect behavior.", - "explanation": "JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic. \n`Data.tuple` provides immutable tuples with structural equality, making them ideal for domain modeling and functional programming patterns.", - "content": "# Working with Tuples using `Data.tuple`\n\n## Guideline\n\nUse `Data.tuple` to create immutable, type-safe tuples that support value-based equality and pattern matching. \nThis is useful for modeling fixed-size, heterogeneous collections of values in a safe and expressive way.\n\n## Rationale\n\nJavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic. \n`Data.tuple` provides immutable tuples with structural equality, making them ideal for domain modeling and functional programming patterns.\n\n## Good Example\n\n```typescript\nimport { Data, Equal } from \"effect\";\n\n// Create two structurally equal tuples\nconst t1 = Data.tuple(1, \"Alice\");\nconst t2 = Data.tuple(1, \"Alice\");\n\n// Compare by value, not reference\nconst areEqual = Equal.equals(t1, t2); // true\n\n// Use tuples as keys in a HashSet or Map\nimport { HashSet } from \"effect\";\nconst set = HashSet.make(t1);\nconsole.log(HashSet.has(set, t2)); // true\n\n// Pattern matching on tuples\nconst [id, name] = t1; // id: number, name: string\n```\n\n**Explanation:**\n\n- `Data.tuple` creates immutable tuples with value-based equality.\n- Useful for modeling pairs, coordinates, or any fixed-size, heterogeneous data.\n- Supports safe pattern matching and collection operations.\n\n## Anti-Pattern\n\nUsing plain arrays for value-based logic or as keys in sets/maps, which compares by reference and can lead to incorrect behavior." - }, - { - "id": "wrap-asynchronous-computations", - "title": "Wrap Asynchronous Computations with tryPromise", - "description": "Wrap asynchronous computations with tryPromise.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define error type using Data.TaggedError\nclass HttpError extends Data.TaggedError(\"HttpError\")<{\n readonly message: string;\n}> {}\n\n// Define HTTP client service\nexport class HttpClient extends Effect.Service()(\"HttpClient\", {\n // Provide default implementation\n sync: () => ({\n getUrl: (url: string) =>\n Effect.tryPromise({\n try: () => fetch(url),\n catch: (error) =>\n new HttpError({ message: `Failed to fetch ${url}: ${error}` }),\n }),\n }),\n}) {}\n\n// Mock HTTP client for demonstration\nexport class MockHttpClient extends Effect.Service()(\n \"MockHttpClient\",\n {\n sync: () => ({\n getUrl: (url: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching URL: ${url}`);\n\n // Simulate different responses based on URL\n if (url.includes(\"success\")) {\n yield* Effect.logInfo(\"✅ Request successful\");\n return new Response(JSON.stringify({ data: \"success\" }), {\n status: 200,\n });\n } else if (url.includes(\"error\")) {\n yield* Effect.logInfo(\"❌ Request failed\");\n return yield* Effect.fail(\n new HttpError({ message: \"Server returned 500\" })\n );\n } else {\n yield* Effect.logInfo(\"✅ Request completed\");\n return new Response(JSON.stringify({ data: \"mock response\" }), {\n status: 200,\n });\n }\n }),\n }),\n }\n) {}\n\n// Demonstrate wrapping asynchronous computations\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Wrapping Asynchronous Computations Demo ===\");\n\n const client = yield* MockHttpClient;\n\n // Example 1: Successful request\n yield* Effect.logInfo(\"\\n1. Successful request:\");\n const response1 = yield* client\n .getUrl(\"https://api.example.com/success\")\n .pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Request failed: ${error.message}`);\n return new Response(\"Error response\", { status: 500 });\n })\n )\n );\n yield* Effect.logInfo(`Response status: ${response1.status}`);\n\n // Example 2: Failed request with error handling\n yield* Effect.logInfo(\"\\n2. Failed request with error handling:\");\n const response2 = yield* client.getUrl(\"https://api.example.com/error\").pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Request failed: ${error.message}`);\n return new Response(\"Fallback response\", { status: 200 });\n })\n )\n );\n yield* Effect.logInfo(`Fallback response status: ${response2.status}`);\n\n // Example 3: Multiple async operations\n yield* Effect.logInfo(\"\\n3. Multiple async operations:\");\n const results = yield* Effect.all(\n [\n client.getUrl(\"https://api.example.com/endpoint1\"),\n client.getUrl(\"https://api.example.com/endpoint2\"),\n client.getUrl(\"https://api.example.com/endpoint3\"),\n ],\n { concurrency: 2 }\n ).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`One or more requests failed: ${error.message}`);\n return [];\n })\n )\n );\n yield* Effect.logInfo(`Completed ${results.length} requests`);\n\n yield* Effect.logInfo(\n \"\\n✅ Asynchronous computations demonstration completed!\"\n );\n});\n\n// Run with mock implementation\nEffect.runPromise(Effect.provide(program, MockHttpClient.Default));\n```\n\n**Explanation:** \n`Effect.tryPromise` wraps a `Promise`-returning function and safely handles\nrejections, moving errors into the Effect's error channel.", - "antiPattern": "Manually handling `.then()` and `.catch()` inside an `Effect.sync`. This is\nverbose, error-prone, and defeats the purpose of using Effect's built-in\nPromise integration.", - "explanation": "This is the standard bridge from the Promise-based world to Effect, allowing\nyou to leverage the massive `async/await` ecosystem safely.", - "content": "# Wrap Asynchronous Computations with tryPromise\n\n## Guideline\n\nTo integrate a `Promise`-based function (like `fetch`), use `Effect.tryPromise`.\n\n## Rationale\n\nThis is the standard bridge from the Promise-based world to Effect, allowing\nyou to leverage the massive `async/await` ecosystem safely.\n\n## Good Example\n\n```typescript\nimport { Effect, Data } from \"effect\";\n\n// Define error type using Data.TaggedError\nclass HttpError extends Data.TaggedError(\"HttpError\")<{\n readonly message: string;\n}> {}\n\n// Define HTTP client service\nexport class HttpClient extends Effect.Service()(\"HttpClient\", {\n // Provide default implementation\n sync: () => ({\n getUrl: (url: string) =>\n Effect.tryPromise({\n try: () => fetch(url),\n catch: (error) =>\n new HttpError({ message: `Failed to fetch ${url}: ${error}` }),\n }),\n }),\n}) {}\n\n// Mock HTTP client for demonstration\nexport class MockHttpClient extends Effect.Service()(\n \"MockHttpClient\",\n {\n sync: () => ({\n getUrl: (url: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching URL: ${url}`);\n\n // Simulate different responses based on URL\n if (url.includes(\"success\")) {\n yield* Effect.logInfo(\"✅ Request successful\");\n return new Response(JSON.stringify({ data: \"success\" }), {\n status: 200,\n });\n } else if (url.includes(\"error\")) {\n yield* Effect.logInfo(\"❌ Request failed\");\n return yield* Effect.fail(\n new HttpError({ message: \"Server returned 500\" })\n );\n } else {\n yield* Effect.logInfo(\"✅ Request completed\");\n return new Response(JSON.stringify({ data: \"mock response\" }), {\n status: 200,\n });\n }\n }),\n }),\n }\n) {}\n\n// Demonstrate wrapping asynchronous computations\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Wrapping Asynchronous Computations Demo ===\");\n\n const client = yield* MockHttpClient;\n\n // Example 1: Successful request\n yield* Effect.logInfo(\"\\n1. Successful request:\");\n const response1 = yield* client\n .getUrl(\"https://api.example.com/success\")\n .pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Request failed: ${error.message}`);\n return new Response(\"Error response\", { status: 500 });\n })\n )\n );\n yield* Effect.logInfo(`Response status: ${response1.status}`);\n\n // Example 2: Failed request with error handling\n yield* Effect.logInfo(\"\\n2. Failed request with error handling:\");\n const response2 = yield* client.getUrl(\"https://api.example.com/error\").pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Request failed: ${error.message}`);\n return new Response(\"Fallback response\", { status: 200 });\n })\n )\n );\n yield* Effect.logInfo(`Fallback response status: ${response2.status}`);\n\n // Example 3: Multiple async operations\n yield* Effect.logInfo(\"\\n3. Multiple async operations:\");\n const results = yield* Effect.all(\n [\n client.getUrl(\"https://api.example.com/endpoint1\"),\n client.getUrl(\"https://api.example.com/endpoint2\"),\n client.getUrl(\"https://api.example.com/endpoint3\"),\n ],\n { concurrency: 2 }\n ).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`One or more requests failed: ${error.message}`);\n return [];\n })\n )\n );\n yield* Effect.logInfo(`Completed ${results.length} requests`);\n\n yield* Effect.logInfo(\n \"\\n✅ Asynchronous computations demonstration completed!\"\n );\n});\n\n// Run with mock implementation\nEffect.runPromise(Effect.provide(program, MockHttpClient.Default));\n```\n\n**Explanation:** \n`Effect.tryPromise` wraps a `Promise`-returning function and safely handles\nrejections, moving errors into the Effect's error channel.\n\n## Anti-Pattern\n\nManually handling `.then()` and `.catch()` inside an `Effect.sync`. This is\nverbose, error-prone, and defeats the purpose of using Effect's built-in\nPromise integration." - }, - { - "id": "wrap-synchronous-computations", - "title": "Wrap Synchronous Computations with sync and try", - "description": "Wrap synchronous computations with sync and try.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\nconst randomNumber = Effect.sync(() => Math.random());\n\nconst parseJson = (input: string) =>\n Effect.try({\n try: () => JSON.parse(input),\n catch: (error) => new Error(`JSON parsing failed: ${error}`),\n });\n\n// More examples of wrapping synchronous computations\nconst divide = (a: number, b: number) =>\n Effect.try({\n try: () => {\n if (b === 0) throw new Error(\"Division by zero\");\n return a / b;\n },\n catch: (error) => new Error(`Division failed: ${error}`),\n });\n\nconst processString = (str: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`Processing string: \"${str}\"`);\n return str.toUpperCase().split(\"\").reverse().join(\"\");\n });\n\n// Demonstrate wrapping synchronous computations\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Wrapping Synchronous Computations Demo ===\");\n\n // Example 1: Basic sync computation\n yield* Effect.log(\"\\n1. Basic sync computation (random number):\");\n const random1 = yield* randomNumber;\n const random2 = yield* randomNumber;\n yield* Effect.log(\n `Random numbers: ${random1.toFixed(4)}, ${random2.toFixed(4)}`\n );\n\n // Example 2: Successful JSON parsing\n yield* Effect.log(\"\\n2. Successful JSON parsing:\");\n const validJson = '{\"name\": \"Paul\", \"age\": 30}';\n const parsed = yield* parseJson(validJson);\n yield* Effect.log(\"Parsed JSON:\" + JSON.stringify(parsed));\n\n // Example 3: Failed JSON parsing with error logging\n yield* Effect.log(\"\\n3. Failed JSON parsing with error logging:\");\n const invalidJson = '{\"name\": \"Paul\", \"age\":}';\n yield* parseJson(invalidJson).pipe(\n Effect.tapError((error) => Effect.log(`Parsing failed: ${error.message}`)),\n Effect.catchAll(() => Effect.succeed({ name: \"default\", age: 0 }))\n );\n yield* Effect.log(\"Continued after error (with recovery)\");\n\n // Example 4: Division with error logging and recovery\n yield* Effect.log(\"\\n4. Division with error logging and recovery:\");\n const division1 = yield* divide(10, 2);\n yield* Effect.log(`10 / 2 = ${division1}`);\n\n // Use tapError to log, then catchAll to recover\n const division2 = yield* divide(10, 0).pipe(\n Effect.tapError((error) => Effect.log(`Division error: ${error.message}`)),\n Effect.catchAll(() => Effect.succeed(-1))\n );\n yield* Effect.log(`10 / 0 = ${division2} (error handled)`);\n\n // Example 5: String processing\n yield* Effect.log(\"\\n5. String processing:\");\n const processed = yield* processString(\"Hello Effect\");\n yield* Effect.log(`Processed result: \"${processed}\"`);\n\n // Example 6: Combining multiple sync operations\n yield* Effect.log(\"\\n6. Combining multiple sync operations:\");\n const combined = yield* Effect.gen(function* () {\n const num = yield* randomNumber;\n const multiplied = yield* Effect.sync(() => num * 100);\n const rounded = yield* Effect.sync(() => Math.round(multiplied));\n return rounded;\n });\n yield* Effect.log(`Combined operations result: ${combined}`);\n\n yield* Effect.log(\"\\n✅ Synchronous computations demonstration completed!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `Effect.sync` for safe synchronous code, and `Effect.try` to safely\nhandle exceptions from potentially unsafe code.", - "antiPattern": "Never use `Effect.sync` for an operation that could throw, like `JSON.parse`.\nThis can lead to unhandled exceptions that crash your application.", - "explanation": "This is the primary way to safely integrate with synchronous libraries like\n`JSON.parse`. `Effect.try` captures any thrown exception and moves it into\nthe Effect's error channel.", - "content": "# Wrap Synchronous Computations with sync and try\n\n## Guideline\n\nTo bring a synchronous side-effect into Effect, wrap it in a thunk (`() => ...`).\nUse `Effect.sync` for functions guaranteed not to throw, and `Effect.try` for\nfunctions that might throw.\n\n## Rationale\n\nThis is the primary way to safely integrate with synchronous libraries like\n`JSON.parse`. `Effect.try` captures any thrown exception and moves it into\nthe Effect's error channel.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\nconst randomNumber = Effect.sync(() => Math.random());\n\nconst parseJson = (input: string) =>\n Effect.try({\n try: () => JSON.parse(input),\n catch: (error) => new Error(`JSON parsing failed: ${error}`),\n });\n\n// More examples of wrapping synchronous computations\nconst divide = (a: number, b: number) =>\n Effect.try({\n try: () => {\n if (b === 0) throw new Error(\"Division by zero\");\n return a / b;\n },\n catch: (error) => new Error(`Division failed: ${error}`),\n });\n\nconst processString = (str: string) =>\n Effect.gen(function* () {\n yield* Effect.log(`Processing string: \"${str}\"`);\n return str.toUpperCase().split(\"\").reverse().join(\"\");\n });\n\n// Demonstrate wrapping synchronous computations\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Wrapping Synchronous Computations Demo ===\");\n\n // Example 1: Basic sync computation\n yield* Effect.log(\"\\n1. Basic sync computation (random number):\");\n const random1 = yield* randomNumber;\n const random2 = yield* randomNumber;\n yield* Effect.log(\n `Random numbers: ${random1.toFixed(4)}, ${random2.toFixed(4)}`\n );\n\n // Example 2: Successful JSON parsing\n yield* Effect.log(\"\\n2. Successful JSON parsing:\");\n const validJson = '{\"name\": \"Paul\", \"age\": 30}';\n const parsed = yield* parseJson(validJson);\n yield* Effect.log(\"Parsed JSON:\" + JSON.stringify(parsed));\n\n // Example 3: Failed JSON parsing with error logging\n yield* Effect.log(\"\\n3. Failed JSON parsing with error logging:\");\n const invalidJson = '{\"name\": \"Paul\", \"age\":}';\n yield* parseJson(invalidJson).pipe(\n Effect.tapError((error) => Effect.log(`Parsing failed: ${error.message}`)),\n Effect.catchAll(() => Effect.succeed({ name: \"default\", age: 0 }))\n );\n yield* Effect.log(\"Continued after error (with recovery)\");\n\n // Example 4: Division with error logging and recovery\n yield* Effect.log(\"\\n4. Division with error logging and recovery:\");\n const division1 = yield* divide(10, 2);\n yield* Effect.log(`10 / 2 = ${division1}`);\n\n // Use tapError to log, then catchAll to recover\n const division2 = yield* divide(10, 0).pipe(\n Effect.tapError((error) => Effect.log(`Division error: ${error.message}`)),\n Effect.catchAll(() => Effect.succeed(-1))\n );\n yield* Effect.log(`10 / 0 = ${division2} (error handled)`);\n\n // Example 5: String processing\n yield* Effect.log(\"\\n5. String processing:\");\n const processed = yield* processString(\"Hello Effect\");\n yield* Effect.log(`Processed result: \"${processed}\"`);\n\n // Example 6: Combining multiple sync operations\n yield* Effect.log(\"\\n6. Combining multiple sync operations:\");\n const combined = yield* Effect.gen(function* () {\n const num = yield* randomNumber;\n const multiplied = yield* Effect.sync(() => num * 100);\n const rounded = yield* Effect.sync(() => Math.round(multiplied));\n return rounded;\n });\n yield* Effect.log(`Combined operations result: ${combined}`);\n\n yield* Effect.log(\"\\n✅ Synchronous computations demonstration completed!\");\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \nUse `Effect.sync` for safe synchronous code, and `Effect.try` to safely\nhandle exceptions from potentially unsafe code.\n\n## Anti-Pattern\n\nNever use `Effect.sync` for an operation that could throw, like `JSON.parse`.\nThis can lead to unhandled exceptions that crash your application." - }, - { - "id": "constructor-try-trypromise", - "title": "Wrapping Synchronous and Asynchronous Computations", - "description": "Use try and tryPromise to lift code that may throw or reject into Effect, capturing errors in the failure channel.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Synchronous: Wrap code that may throw\nconst effectSync = Effect.try({\n try: () => JSON.parse(\"{ invalid json }\"),\n catch: (error) => `Parse error: ${String(error)}`,\n}); // Effect\n\n// Asynchronous: Wrap a promise that may reject\nconst effectAsync = Effect.tryPromise({\n try: () => fetch(\"https://api.example.com/data\").then((res) => res.json()),\n catch: (error) => `Network error: ${String(error)}`,\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.try` wraps a synchronous computation that may throw, capturing the error in the failure channel.\n- `Effect.tryPromise` wraps an async computation (Promise) that may reject, capturing the rejection as a failure.", - "antiPattern": "Using try/catch for error handling, or relying on untyped Promise rejections, which leads to less composable and less type-safe code.", - "explanation": "Wrapping potentially unsafe code in `try` or `tryPromise` ensures that all errors are handled in a uniform, declarative way. \nThis eliminates the need for try/catch blocks and makes error handling explicit and type-safe.", - "content": "# Wrapping Synchronous and Asynchronous Computations\n\n## Guideline\n\nUse the `try` and `tryPromise` constructors to safely wrap synchronous or asynchronous computations that may throw exceptions or reject promises. \nThis captures errors in the Effect failure channel, making them type-safe and composable.\n\n## Rationale\n\nWrapping potentially unsafe code in `try` or `tryPromise` ensures that all errors are handled in a uniform, declarative way. \nThis eliminates the need for try/catch blocks and makes error handling explicit and type-safe.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Synchronous: Wrap code that may throw\nconst effectSync = Effect.try({\n try: () => JSON.parse(\"{ invalid json }\"),\n catch: (error) => `Parse error: ${String(error)}`,\n}); // Effect\n\n// Asynchronous: Wrap a promise that may reject\nconst effectAsync = Effect.tryPromise({\n try: () => fetch(\"https://api.example.com/data\").then((res) => res.json()),\n catch: (error) => `Network error: ${String(error)}`,\n}); // Effect\n```\n\n**Explanation:**\n\n- `Effect.try` wraps a synchronous computation that may throw, capturing the error in the failure channel.\n- `Effect.tryPromise` wraps an async computation (Promise) that may reject, capturing the rejection as a failure.\n\n## Anti-Pattern\n\nUsing try/catch for error handling, or relying on untyped Promise rejections, which leads to less composable and less type-safe code." - }, - { - "id": "write-sequential-code-with-gen", - "title": "Write Sequential Code with Effect.gen", - "description": "Write sequential code with Effect.gen.", - "skillLevel": "beginner", - "useCases": [ - "core-concepts" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Mock API functions for demonstration\nconst fetchUser = (id: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user ${id}...`);\n // Simulate API call\n yield* Effect.sleep(\"100 millis\");\n return { id, name: `User ${id}`, email: `user${id}@example.com` };\n });\n\nconst fetchUserPosts = (userId: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching posts for user ${userId}...`);\n // Simulate API call\n yield* Effect.sleep(\"150 millis\");\n return [\n { id: 1, title: \"First Post\", userId },\n { id: 2, title: \"Second Post\", userId },\n ];\n });\n\nconst fetchPostComments = (postId: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching comments for post ${postId}...`);\n // Simulate API call\n yield* Effect.sleep(\"75 millis\");\n return [\n { id: 1, text: \"Great post!\", postId },\n { id: 2, text: \"Thanks for sharing\", postId },\n ];\n });\n\n// Example of sequential code with Effect.gen\nconst getUserDataWithGen = (userId: number) =>\n Effect.gen(function* () {\n // Step 1: Fetch user\n const user = yield* fetchUser(userId);\n yield* Effect.logInfo(`✅ Got user: ${user.name}`);\n\n // Step 2: Fetch user's posts (depends on user data)\n const posts = yield* fetchUserPosts(user.id);\n yield* Effect.logInfo(`✅ Got ${posts.length} posts`);\n\n // Step 3: Fetch comments for first post (depends on posts data)\n const firstPost = posts[0];\n const comments = yield* fetchPostComments(firstPost.id);\n yield* Effect.logInfo(\n `✅ Got ${comments.length} comments for \"${firstPost.title}\"`\n );\n\n // Step 4: Combine all data\n const result = {\n user,\n posts,\n featuredPost: {\n ...firstPost,\n comments,\n },\n };\n\n yield* Effect.logInfo(\"✅ Successfully combined all user data\");\n return result;\n });\n\n// Example without Effect.gen (more complex)\nconst getUserDataWithoutGen = (userId: number) =>\n fetchUser(userId).pipe(\n Effect.flatMap((user) =>\n fetchUserPosts(user.id).pipe(\n Effect.flatMap((posts) =>\n fetchPostComments(posts[0].id).pipe(\n Effect.map((comments) => ({\n user,\n posts,\n featuredPost: {\n ...posts[0],\n comments,\n },\n }))\n )\n )\n )\n )\n );\n\n// Demonstrate writing sequential code with gen\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Writing Sequential Code with Effect.gen Demo ===\");\n\n // Example 1: Sequential operations with Effect.gen\n yield* Effect.logInfo(\"\\n1. Sequential operations with Effect.gen:\");\n const userData = yield* getUserDataWithGen(123).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to get user data: ${error}`);\n return null;\n })\n )\n );\n\n if (userData) {\n yield* Effect.logInfo(\n `Final result: User \"${userData.user.name}\" has ${userData.posts.length} posts`\n );\n yield* Effect.logInfo(\n `Featured post: \"${userData.featuredPost.title}\" with ${userData.featuredPost.comments.length} comments`\n );\n }\n\n // Example 2: Compare with traditional promise-like chaining\n yield* Effect.logInfo(\"\\n2. Same logic without Effect.gen (for comparison):\");\n const userData2 = yield* getUserDataWithoutGen(456).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to get user data: ${error}`);\n return null;\n })\n )\n );\n\n if (userData2) {\n yield* Effect.logInfo(\n `Result from traditional approach: User \"${userData2.user.name}\"`\n );\n }\n\n // Example 3: Error handling in sequential code\n yield* Effect.logInfo(\"\\n3. Error handling in sequential operations:\");\n const errorHandling = yield* Effect.gen(function* () {\n try {\n const user = yield* fetchUser(999);\n const posts = yield* fetchUserPosts(user.id);\n return { user, posts };\n } catch (error) {\n yield* Effect.logError(`Error in sequential operations: ${error}`);\n return null;\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Caught error: ${error}`);\n return { user: null, posts: [] };\n })\n )\n );\n\n yield* Effect.logInfo(\n `Error handling result: ${errorHandling ? \"Success\" : \"Handled error\"}`\n );\n\n yield* Effect.logInfo(\"\\n✅ Sequential code demonstration completed!\");\n yield* Effect.logInfo(\n \"Effect.gen makes sequential async code look like synchronous code!\"\n );\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.gen` allows you to write top-to-bottom code that is easy to read and\nmaintain, even when chaining many asynchronous steps.", - "antiPattern": "Deeply nesting `flatMap` calls. This is much harder to read and maintain than\nthe equivalent `Effect.gen` block.", - "explanation": "`Effect.gen` uses generator functions to create a flat, linear, and highly\nreadable sequence of operations, avoiding the nested \"callback hell\" of\n`flatMap`.", - "content": "# Write Sequential Code with Effect.gen\n\n## Guideline\n\nFor sequential operations that depend on each other, use `Effect.gen` to write\nyour logic in a familiar, imperative style. It's the Effect-native equivalent\nof `async/await`.\n\n## Rationale\n\n`Effect.gen` uses generator functions to create a flat, linear, and highly\nreadable sequence of operations, avoiding the nested \"callback hell\" of\n`flatMap`.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Mock API functions for demonstration\nconst fetchUser = (id: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching user ${id}...`);\n // Simulate API call\n yield* Effect.sleep(\"100 millis\");\n return { id, name: `User ${id}`, email: `user${id}@example.com` };\n });\n\nconst fetchUserPosts = (userId: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching posts for user ${userId}...`);\n // Simulate API call\n yield* Effect.sleep(\"150 millis\");\n return [\n { id: 1, title: \"First Post\", userId },\n { id: 2, title: \"Second Post\", userId },\n ];\n });\n\nconst fetchPostComments = (postId: number) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(`Fetching comments for post ${postId}...`);\n // Simulate API call\n yield* Effect.sleep(\"75 millis\");\n return [\n { id: 1, text: \"Great post!\", postId },\n { id: 2, text: \"Thanks for sharing\", postId },\n ];\n });\n\n// Example of sequential code with Effect.gen\nconst getUserDataWithGen = (userId: number) =>\n Effect.gen(function* () {\n // Step 1: Fetch user\n const user = yield* fetchUser(userId);\n yield* Effect.logInfo(`✅ Got user: ${user.name}`);\n\n // Step 2: Fetch user's posts (depends on user data)\n const posts = yield* fetchUserPosts(user.id);\n yield* Effect.logInfo(`✅ Got ${posts.length} posts`);\n\n // Step 3: Fetch comments for first post (depends on posts data)\n const firstPost = posts[0];\n const comments = yield* fetchPostComments(firstPost.id);\n yield* Effect.logInfo(\n `✅ Got ${comments.length} comments for \"${firstPost.title}\"`\n );\n\n // Step 4: Combine all data\n const result = {\n user,\n posts,\n featuredPost: {\n ...firstPost,\n comments,\n },\n };\n\n yield* Effect.logInfo(\"✅ Successfully combined all user data\");\n return result;\n });\n\n// Example without Effect.gen (more complex)\nconst getUserDataWithoutGen = (userId: number) =>\n fetchUser(userId).pipe(\n Effect.flatMap((user) =>\n fetchUserPosts(user.id).pipe(\n Effect.flatMap((posts) =>\n fetchPostComments(posts[0].id).pipe(\n Effect.map((comments) => ({\n user,\n posts,\n featuredPost: {\n ...posts[0],\n comments,\n },\n }))\n )\n )\n )\n )\n );\n\n// Demonstrate writing sequential code with gen\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\"=== Writing Sequential Code with Effect.gen Demo ===\");\n\n // Example 1: Sequential operations with Effect.gen\n yield* Effect.logInfo(\"\\n1. Sequential operations with Effect.gen:\");\n const userData = yield* getUserDataWithGen(123).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to get user data: ${error}`);\n return null;\n })\n )\n );\n\n if (userData) {\n yield* Effect.logInfo(\n `Final result: User \"${userData.user.name}\" has ${userData.posts.length} posts`\n );\n yield* Effect.logInfo(\n `Featured post: \"${userData.featuredPost.title}\" with ${userData.featuredPost.comments.length} comments`\n );\n }\n\n // Example 2: Compare with traditional promise-like chaining\n yield* Effect.logInfo(\"\\n2. Same logic without Effect.gen (for comparison):\");\n const userData2 = yield* getUserDataWithoutGen(456).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Failed to get user data: ${error}`);\n return null;\n })\n )\n );\n\n if (userData2) {\n yield* Effect.logInfo(\n `Result from traditional approach: User \"${userData2.user.name}\"`\n );\n }\n\n // Example 3: Error handling in sequential code\n yield* Effect.logInfo(\"\\n3. Error handling in sequential operations:\");\n const errorHandling = yield* Effect.gen(function* () {\n try {\n const user = yield* fetchUser(999);\n const posts = yield* fetchUserPosts(user.id);\n return { user, posts };\n } catch (error) {\n yield* Effect.logError(`Error in sequential operations: ${error}`);\n return null;\n }\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logError(`Caught error: ${error}`);\n return { user: null, posts: [] };\n })\n )\n );\n\n yield* Effect.logInfo(\n `Error handling result: ${errorHandling ? \"Success\" : \"Handled error\"}`\n );\n\n yield* Effect.logInfo(\"\\n✅ Sequential code demonstration completed!\");\n yield* Effect.logInfo(\n \"Effect.gen makes sequential async code look like synchronous code!\"\n );\n});\n\nEffect.runPromise(program);\n```\n\n**Explanation:** \n`Effect.gen` allows you to write top-to-bottom code that is easy to read and\nmaintain, even when chaining many asynchronous steps.\n\n## Anti-Pattern\n\nDeeply nesting `flatMap` calls. This is much harder to read and maintain than\nthe equivalent `Effect.gen` block." - }, - { - "id": "write-tests-that-adapt-to-application-code", - "title": "Write Tests That Adapt to Application Code", - "description": "Write tests that adapt to application code.", - "skillLevel": "intermediate", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { Effect } from \"effect\";\n\n// Define our types\ninterface User {\n id: number;\n name: string;\n}\n\nclass NotFoundError extends Error {\n readonly _tag = \"NotFoundError\";\n constructor(readonly id: number) {\n super(`User ${id} not found`);\n }\n}\n\n// Define database service interface\ninterface DatabaseServiceApi {\n getUserById: (id: number) => Effect.Effect;\n}\n\n// Implement the service with mock data\nclass DatabaseService extends Effect.Service()(\n \"DatabaseService\",\n {\n sync: () => ({\n getUserById: (id: number) => {\n // Simulate database lookup\n if (id === 404) {\n return Effect.fail(new NotFoundError(id));\n }\n return Effect.succeed({ id, name: `User ${id}` });\n },\n }),\n }\n) {}\n\n// Test service implementation for testing\nclass TestDatabaseService extends Effect.Service()(\n \"TestDatabaseService\",\n {\n sync: () => ({\n getUserById: (id: number) => {\n // Test data with predictable responses\n const testUsers = [\n { id: 1, name: \"Test User 1\" },\n { id: 2, name: \"Test User 2\" },\n { id: 123, name: \"User 123\" },\n ];\n\n const user = testUsers.find((u) => u.id === id);\n if (user) {\n return Effect.succeed(user);\n }\n return Effect.fail(new NotFoundError(id));\n },\n }),\n }\n) {}\n\n// Business logic that uses the database service\nconst getUserWithFallback = (id: number) =>\n Effect.gen(function* () {\n const db = yield* DatabaseService;\n return yield* Effect.gen(function* () {\n const user = yield* db.getUserById(id);\n return user;\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(`User ${id} not found, using fallback`);\n return { id, name: `Fallback User ${id}` };\n }\n return yield* Effect.fail(error);\n })\n )\n );\n });\n\n// Create a program that demonstrates the service\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\n \"=== Writing Tests that Adapt to Application Code Demo ===\"\n );\n\n const db = yield* DatabaseService;\n\n // Example 1: Successful user lookup\n yield* Effect.logInfo(\"\\n1. Looking up existing user 123...\");\n const user = yield* Effect.gen(function* () {\n try {\n return yield* db.getUserById(123);\n } catch (error) {\n yield* Effect.logError(\n `Failed to get user: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Error\" };\n }\n });\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n\n // Example 2: Handle non-existent user with proper error handling\n yield* Effect.logInfo(\"\\n2. Looking up non-existent user 404...\");\n const notFoundUser = yield* Effect.gen(function* () {\n try {\n return yield* db.getUserById(404);\n } catch (error) {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(\n `✅ Properly handled NotFoundError: ${error.message}`\n );\n return { id: 404, name: \"Not Found\" };\n }\n yield* Effect.logError(\n `Unexpected error: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Error\" };\n }\n });\n yield* Effect.logInfo(`Result: ${JSON.stringify(notFoundUser)}`);\n\n // Example 3: Business logic with fallback\n yield* Effect.logInfo(\"\\n3. Business logic with fallback for missing user:\");\n const userWithFallback = yield* getUserWithFallback(999);\n yield* Effect.logInfo(\n `User with fallback: ${JSON.stringify(userWithFallback)}`\n );\n\n // Example 4: Testing with different service implementation\n yield* Effect.logInfo(\"\\n4. Testing with test service implementation:\");\n yield* Effect.provide(\n Effect.gen(function* () {\n const testDb = yield* TestDatabaseService;\n\n // Test existing user\n const testUser1 = yield* Effect.gen(function* () {\n try {\n return yield* testDb.getUserById(1);\n } catch (error) {\n yield* Effect.logError(\n `Test failed: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Test Error\" };\n }\n });\n yield* Effect.logInfo(`Test user 1: ${JSON.stringify(testUser1)}`);\n\n // Test non-existing user\n const testUser404 = yield* Effect.gen(function* () {\n try {\n return yield* testDb.getUserById(404);\n } catch (error) {\n yield* Effect.logInfo(\n `✅ Test service properly threw NotFoundError: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: 404, name: \"Test Not Found\" };\n }\n });\n yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`);\n }),\n TestDatabaseService.Default\n );\n\n yield* Effect.logInfo(\n \"\\n✅ Tests that adapt to application code demonstration completed!\"\n );\n yield* Effect.logInfo(\n \"The same business logic works with different service implementations!\"\n );\n});\n\n// Run the program with the default database service\nEffect.runPromise(\n Effect.provide(program, DatabaseService.Default) as Effect.Effect<\n void,\n never,\n never\n >\n);\n```\n\n**Explanation:** \nTests should reflect the real interface and behavior of your code, not force changes to it.", - "antiPattern": "Any action where the test dictates a change to the application code. Do not modify a service file to add a method just because a test needs it. If a test fails, fix the test.", - "explanation": "Treating application code as immutable during testing prevents the introduction of bugs and false test confidence. The goal of a test is to verify real-world behavior; changing that behavior to suit the test invalidates its purpose.", - "content": "# Write Tests That Adapt to Application Code\n\n## Guideline\n\nTests are secondary artifacts that serve to validate the application. The application's code and interfaces are the source of truth. When a test fails, fix the test's logic or setup, not the production code.\n\n## Rationale\n\nTreating application code as immutable during testing prevents the introduction of bugs and false test confidence. The goal of a test is to verify real-world behavior; changing that behavior to suit the test invalidates its purpose.\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\";\n\n// Define our types\ninterface User {\n id: number;\n name: string;\n}\n\nclass NotFoundError extends Error {\n readonly _tag = \"NotFoundError\";\n constructor(readonly id: number) {\n super(`User ${id} not found`);\n }\n}\n\n// Define database service interface\ninterface DatabaseServiceApi {\n getUserById: (id: number) => Effect.Effect;\n}\n\n// Implement the service with mock data\nclass DatabaseService extends Effect.Service()(\n \"DatabaseService\",\n {\n sync: () => ({\n getUserById: (id: number) => {\n // Simulate database lookup\n if (id === 404) {\n return Effect.fail(new NotFoundError(id));\n }\n return Effect.succeed({ id, name: `User ${id}` });\n },\n }),\n }\n) {}\n\n// Test service implementation for testing\nclass TestDatabaseService extends Effect.Service()(\n \"TestDatabaseService\",\n {\n sync: () => ({\n getUserById: (id: number) => {\n // Test data with predictable responses\n const testUsers = [\n { id: 1, name: \"Test User 1\" },\n { id: 2, name: \"Test User 2\" },\n { id: 123, name: \"User 123\" },\n ];\n\n const user = testUsers.find((u) => u.id === id);\n if (user) {\n return Effect.succeed(user);\n }\n return Effect.fail(new NotFoundError(id));\n },\n }),\n }\n) {}\n\n// Business logic that uses the database service\nconst getUserWithFallback = (id: number) =>\n Effect.gen(function* () {\n const db = yield* DatabaseService;\n return yield* Effect.gen(function* () {\n const user = yield* db.getUserById(id);\n return user;\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(`User ${id} not found, using fallback`);\n return { id, name: `Fallback User ${id}` };\n }\n return yield* Effect.fail(error);\n })\n )\n );\n });\n\n// Create a program that demonstrates the service\nconst program = Effect.gen(function* () {\n yield* Effect.logInfo(\n \"=== Writing Tests that Adapt to Application Code Demo ===\"\n );\n\n const db = yield* DatabaseService;\n\n // Example 1: Successful user lookup\n yield* Effect.logInfo(\"\\n1. Looking up existing user 123...\");\n const user = yield* Effect.gen(function* () {\n try {\n return yield* db.getUserById(123);\n } catch (error) {\n yield* Effect.logError(\n `Failed to get user: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Error\" };\n }\n });\n yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);\n\n // Example 2: Handle non-existent user with proper error handling\n yield* Effect.logInfo(\"\\n2. Looking up non-existent user 404...\");\n const notFoundUser = yield* Effect.gen(function* () {\n try {\n return yield* db.getUserById(404);\n } catch (error) {\n if (error instanceof NotFoundError) {\n yield* Effect.logInfo(\n `✅ Properly handled NotFoundError: ${error.message}`\n );\n return { id: 404, name: \"Not Found\" };\n }\n yield* Effect.logError(\n `Unexpected error: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Error\" };\n }\n });\n yield* Effect.logInfo(`Result: ${JSON.stringify(notFoundUser)}`);\n\n // Example 3: Business logic with fallback\n yield* Effect.logInfo(\"\\n3. Business logic with fallback for missing user:\");\n const userWithFallback = yield* getUserWithFallback(999);\n yield* Effect.logInfo(\n `User with fallback: ${JSON.stringify(userWithFallback)}`\n );\n\n // Example 4: Testing with different service implementation\n yield* Effect.logInfo(\"\\n4. Testing with test service implementation:\");\n yield* Effect.provide(\n Effect.gen(function* () {\n const testDb = yield* TestDatabaseService;\n\n // Test existing user\n const testUser1 = yield* Effect.gen(function* () {\n try {\n return yield* testDb.getUserById(1);\n } catch (error) {\n yield* Effect.logError(\n `Test failed: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: -1, name: \"Test Error\" };\n }\n });\n yield* Effect.logInfo(`Test user 1: ${JSON.stringify(testUser1)}`);\n\n // Test non-existing user\n const testUser404 = yield* Effect.gen(function* () {\n try {\n return yield* testDb.getUserById(404);\n } catch (error) {\n yield* Effect.logInfo(\n `✅ Test service properly threw NotFoundError: ${error instanceof Error ? error.message : \"Unknown error\"}`\n );\n return { id: 404, name: \"Test Not Found\" };\n }\n });\n yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`);\n }),\n TestDatabaseService.Default\n );\n\n yield* Effect.logInfo(\n \"\\n✅ Tests that adapt to application code demonstration completed!\"\n );\n yield* Effect.logInfo(\n \"The same business logic works with different service implementations!\"\n );\n});\n\n// Run the program with the default database service\nEffect.runPromise(\n Effect.provide(program, DatabaseService.Default) as Effect.Effect<\n void,\n never,\n never\n >\n);\n```\n\n**Explanation:** \nTests should reflect the real interface and behavior of your code, not force changes to it.\n\n## Anti-Pattern\n\nAny action where the test dictates a change to the application code. Do not modify a service file to add a method just because a test needs it. If a test fails, fix the test." - }, - { - "id": "domain-modeling-hello-world", - "title": "Your First Domain Model", - "description": "Start domain modeling by defining clear interfaces for your business entities.", - "skillLevel": "beginner", - "useCases": [ - "domain-modeling" - ], - "example": "```typescript\nimport { Effect } from \"effect\"\n\n// ============================================\n// 1. Define domain entities as interfaces\n// ============================================\n\ninterface User {\n readonly id: string\n readonly email: string\n readonly name: string\n readonly createdAt: Date\n}\n\ninterface Product {\n readonly sku: string\n readonly name: string\n readonly price: number\n readonly inStock: boolean\n}\n\ninterface Order {\n readonly id: string\n readonly userId: string\n readonly items: ReadonlyArray\n readonly total: number\n readonly status: OrderStatus\n}\n\ninterface OrderItem {\n readonly productSku: string\n readonly quantity: number\n readonly unitPrice: number\n}\n\ntype OrderStatus = \"pending\" | \"confirmed\" | \"shipped\" | \"delivered\"\n\n// ============================================\n// 2. Create domain functions\n// ============================================\n\nconst createUser = (email: string, name: string): User => ({\n id: crypto.randomUUID(),\n email,\n name,\n createdAt: new Date(),\n})\n\nconst calculateOrderTotal = (items: ReadonlyArray): number =>\n items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)\n\n// ============================================\n// 3. Use in Effect programs\n// ============================================\n\nconst program = Effect.gen(function* () {\n const user = createUser(\"alice@example.com\", \"Alice\")\n yield* Effect.log(`Created user: ${user.name}`)\n\n const items: OrderItem[] = [\n { productSku: \"WIDGET-001\", quantity: 2, unitPrice: 29.99 },\n { productSku: \"GADGET-002\", quantity: 1, unitPrice: 49.99 },\n ]\n\n const order: Order = {\n id: crypto.randomUUID(),\n userId: user.id,\n items,\n total: calculateOrderTotal(items),\n status: \"pending\",\n }\n\n yield* Effect.log(`Order total: $${order.total.toFixed(2)}`)\n return order\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Good domain modeling:\n\n1. **Clarifies intent** - Types document what data means\n2. **Prevents errors** - Compiler catches wrong data usage\n3. **Enables tooling** - IDE autocompletion and refactoring\n4. **Communicates** - Code becomes documentation\n\n---", - "content": "## Guideline\n\nStart by defining TypeScript interfaces that represent your business entities. Use descriptive names that match your domain language.\n\n---\n\n## Rationale\n\nGood domain modeling:\n\n1. **Clarifies intent** - Types document what data means\n2. **Prevents errors** - Compiler catches wrong data usage\n3. **Enables tooling** - IDE autocompletion and refactoring\n4. **Communicates** - Code becomes documentation\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\"\n\n// ============================================\n// 1. Define domain entities as interfaces\n// ============================================\n\ninterface User {\n readonly id: string\n readonly email: string\n readonly name: string\n readonly createdAt: Date\n}\n\ninterface Product {\n readonly sku: string\n readonly name: string\n readonly price: number\n readonly inStock: boolean\n}\n\ninterface Order {\n readonly id: string\n readonly userId: string\n readonly items: ReadonlyArray\n readonly total: number\n readonly status: OrderStatus\n}\n\ninterface OrderItem {\n readonly productSku: string\n readonly quantity: number\n readonly unitPrice: number\n}\n\ntype OrderStatus = \"pending\" | \"confirmed\" | \"shipped\" | \"delivered\"\n\n// ============================================\n// 2. Create domain functions\n// ============================================\n\nconst createUser = (email: string, name: string): User => ({\n id: crypto.randomUUID(),\n email,\n name,\n createdAt: new Date(),\n})\n\nconst calculateOrderTotal = (items: ReadonlyArray): number =>\n items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0)\n\n// ============================================\n// 3. Use in Effect programs\n// ============================================\n\nconst program = Effect.gen(function* () {\n const user = createUser(\"alice@example.com\", \"Alice\")\n yield* Effect.log(`Created user: ${user.name}`)\n\n const items: OrderItem[] = [\n { productSku: \"WIDGET-001\", quantity: 2, unitPrice: 29.99 },\n { productSku: \"GADGET-002\", quantity: 1, unitPrice: 49.99 },\n ]\n\n const order: Order = {\n id: crypto.randomUUID(),\n userId: user.id,\n items,\n total: calculateOrderTotal(items),\n status: \"pending\",\n }\n\n yield* Effect.log(`Order total: $${order.total.toFixed(2)}`)\n return order\n})\n\nEffect.runPromise(program)\n```\n\n## Key Concepts\n\n| Concept | Explanation |\n|---------|-------------|\n| **Interfaces** | Define the shape of your domain entities |\n| **readonly** | Prevent accidental mutation |\n| **Union types** | Model finite states (OrderStatus) |\n| **Pure functions** | Create and transform entities |\n\n## Best Practices\n\n1. **Use `readonly`** - Immutable by default\n2. **Descriptive names** - Match your business language\n3. **Small interfaces** - Single responsibility\n4. **Union types** - For finite states\n\n## Related Patterns\n\n- [Branded Types](./brand-model-domain-type.mdx)\n- [Tagged Errors](./define-tagged-errors.mdx)\n- [Option for Missing Values](./model-optional-values-with-option.mdx)" - }, - { - "id": "testing-hello-world", - "title": "Your First Effect Test", - "description": "Use Effect.runPromise in tests to run and assert on Effect results.", - "skillLevel": "beginner", - "useCases": [ - "testing" - ], - "example": "```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect } from \"effect\"\n\n// ============================================\n// Code to test\n// ============================================\n\nconst add = (a: number, b: number): Effect.Effect =>\n Effect.succeed(a + b)\n\nconst divide = (a: number, b: number): Effect.Effect =>\n b === 0\n ? Effect.fail(new Error(\"Cannot divide by zero\"))\n : Effect.succeed(a / b)\n\nconst fetchUser = (id: string): Effect.Effect<{ id: string; name: string }> =>\n Effect.succeed({ id, name: `User ${id}` })\n\n// ============================================\n// Tests\n// ============================================\n\ndescribe(\"Basic Effect Tests\", () => {\n it(\"should add two numbers\", async () => {\n const result = await Effect.runPromise(add(2, 3))\n expect(result).toBe(5)\n })\n\n it(\"should divide numbers\", async () => {\n const result = await Effect.runPromise(divide(10, 2))\n expect(result).toBe(5)\n })\n\n it(\"should fail on divide by zero\", async () => {\n await expect(Effect.runPromise(divide(10, 0))).rejects.toThrow(\n \"Cannot divide by zero\"\n )\n })\n\n it(\"should fetch a user\", async () => {\n const user = await Effect.runPromise(fetchUser(\"123\"))\n \n expect(user).toEqual({\n id: \"123\",\n name: \"User 123\",\n })\n })\n})\n\n// ============================================\n// Testing Effect.gen programs\n// ============================================\n\nconst calculateDiscount = (price: number, quantity: number) =>\n Effect.gen(function* () {\n if (price <= 0) {\n return yield* Effect.fail(new Error(\"Invalid price\"))\n }\n \n const subtotal = price * quantity\n const discount = quantity >= 10 ? 0.1 : 0\n const total = subtotal * (1 - discount)\n \n return { subtotal, discount, total }\n })\n\ndescribe(\"Effect.gen Tests\", () => {\n it(\"should calculate without discount\", async () => {\n const result = await Effect.runPromise(calculateDiscount(10, 5))\n \n expect(result.subtotal).toBe(50)\n expect(result.discount).toBe(0)\n expect(result.total).toBe(50)\n })\n\n it(\"should apply bulk discount\", async () => {\n const result = await Effect.runPromise(calculateDiscount(10, 10))\n \n expect(result.subtotal).toBe(100)\n expect(result.discount).toBe(0.1)\n expect(result.total).toBe(90)\n })\n\n it(\"should fail for invalid price\", async () => {\n await expect(\n Effect.runPromise(calculateDiscount(-5, 10))\n ).rejects.toThrow(\"Invalid price\")\n })\n})\n```", - "antiPattern": "", - "explanation": "Testing Effect code is straightforward:\n\n1. **Effects are values** - Build them in tests like any other value\n2. **Run to get results** - Use `Effect.runPromise` to execute\n3. **Assert normally** - Standard assertions work on the results\n\n---", - "content": "## Guideline\n\nTest Effect programs by running them with `Effect.runPromise` and using standard test assertions on the results.\n\n---\n\n## Rationale\n\nTesting Effect code is straightforward:\n\n1. **Effects are values** - Build them in tests like any other value\n2. **Run to get results** - Use `Effect.runPromise` to execute\n3. **Assert normally** - Standard assertions work on the results\n\n---\n\n## Good Example\n\n```typescript\nimport { describe, it, expect } from \"vitest\"\nimport { Effect } from \"effect\"\n\n// ============================================\n// Code to test\n// ============================================\n\nconst add = (a: number, b: number): Effect.Effect =>\n Effect.succeed(a + b)\n\nconst divide = (a: number, b: number): Effect.Effect =>\n b === 0\n ? Effect.fail(new Error(\"Cannot divide by zero\"))\n : Effect.succeed(a / b)\n\nconst fetchUser = (id: string): Effect.Effect<{ id: string; name: string }> =>\n Effect.succeed({ id, name: `User ${id}` })\n\n// ============================================\n// Tests\n// ============================================\n\ndescribe(\"Basic Effect Tests\", () => {\n it(\"should add two numbers\", async () => {\n const result = await Effect.runPromise(add(2, 3))\n expect(result).toBe(5)\n })\n\n it(\"should divide numbers\", async () => {\n const result = await Effect.runPromise(divide(10, 2))\n expect(result).toBe(5)\n })\n\n it(\"should fail on divide by zero\", async () => {\n await expect(Effect.runPromise(divide(10, 0))).rejects.toThrow(\n \"Cannot divide by zero\"\n )\n })\n\n it(\"should fetch a user\", async () => {\n const user = await Effect.runPromise(fetchUser(\"123\"))\n \n expect(user).toEqual({\n id: \"123\",\n name: \"User 123\",\n })\n })\n})\n\n// ============================================\n// Testing Effect.gen programs\n// ============================================\n\nconst calculateDiscount = (price: number, quantity: number) =>\n Effect.gen(function* () {\n if (price <= 0) {\n return yield* Effect.fail(new Error(\"Invalid price\"))\n }\n \n const subtotal = price * quantity\n const discount = quantity >= 10 ? 0.1 : 0\n const total = subtotal * (1 - discount)\n \n return { subtotal, discount, total }\n })\n\ndescribe(\"Effect.gen Tests\", () => {\n it(\"should calculate without discount\", async () => {\n const result = await Effect.runPromise(calculateDiscount(10, 5))\n \n expect(result.subtotal).toBe(50)\n expect(result.discount).toBe(0)\n expect(result.total).toBe(50)\n })\n\n it(\"should apply bulk discount\", async () => {\n const result = await Effect.runPromise(calculateDiscount(10, 10))\n \n expect(result.subtotal).toBe(100)\n expect(result.discount).toBe(0.1)\n expect(result.total).toBe(90)\n })\n\n it(\"should fail for invalid price\", async () => {\n await expect(\n Effect.runPromise(calculateDiscount(-5, 10))\n ).rejects.toThrow(\"Invalid price\")\n })\n})\n```\n\n## Testing Patterns\n\n| Scenario | Approach |\n|----------|----------|\n| Success case | `await Effect.runPromise(effect)` then assert |\n| Failure case | `expect(...).rejects.toThrow()` |\n| Multiple effects | Test each independently |\n| Effect.gen | Same as above - it's still an Effect |\n\n## Best Practices\n\n1. **One assertion per test** - Clear failure messages\n2. **Test success and failure** - Both paths matter\n3. **Descriptive names** - Explain what's being tested\n4. **Arrange-Act-Assert** - Clear test structure" - }, - { - "id": "error-management-hello-world", - "title": "Your First Error Handler", - "description": "Use catchAll or catchTag to recover from errors and keep your program running.", - "skillLevel": "beginner", - "useCases": [ - "error-management" - ], - "example": "```typescript\nimport { Effect, Data } from \"effect\"\n\n// ============================================\n// 1. Define typed errors\n// ============================================\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly url: string\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly resource: string\n}> {}\n\n// ============================================\n// 2. Functions that can fail\n// ============================================\n\nconst fetchData = (url: string): Effect.Effect =>\n url.startsWith(\"http\")\n ? Effect.succeed(`Data from ${url}`)\n : Effect.fail(new NetworkError({ url }))\n\nconst findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> =>\n id === \"123\"\n ? Effect.succeed({ id, name: \"Alice\" })\n : Effect.fail(new NotFoundError({ resource: `user:${id}` }))\n\n// ============================================\n// 3. Handle ALL errors with catchAll\n// ============================================\n\nconst withFallback = fetchData(\"invalid-url\").pipe(\n Effect.catchAll((error) => {\n console.log(`Failed: ${error.url}, using fallback`)\n return Effect.succeed(\"Fallback data\")\n })\n)\n\n// Result: \"Fallback data\"\n\n// ============================================\n// 4. Handle SPECIFIC errors with catchTag\n// ============================================\n\nconst findUserOrDefault = (id: string) =>\n findUser(id).pipe(\n Effect.catchTag(\"NotFoundError\", (error) => {\n console.log(`User not found: ${error.resource}`)\n return Effect.succeed({ id: \"guest\", name: \"Guest User\" })\n })\n )\n\n// ============================================\n// 5. Handle MULTIPLE error types\n// ============================================\n\nconst fetchUser = (url: string, id: string) =>\n Effect.gen(function* () {\n yield* fetchData(url)\n return yield* findUser(id)\n })\n\nconst robustFetchUser = (url: string, id: string) =>\n fetchUser(url, id).pipe(\n Effect.catchTags({\n NetworkError: (e) => Effect.succeed({ id: \"offline\", name: `Offline (${e.url})` }),\n NotFoundError: (e) => Effect.succeed({ id: \"unknown\", name: `Unknown (${e.resource})` }),\n })\n )\n\n// ============================================\n// 6. Run the examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n // catchAll example\n const data = yield* withFallback\n yield* Effect.log(`Got data: ${data}`)\n\n // catchTag example\n const user = yield* findUserOrDefault(\"999\")\n yield* Effect.log(`Got user: ${user.name}`)\n\n // Multiple error types\n const result = yield* robustFetchUser(\"invalid\", \"999\")\n yield* Effect.log(`Robust result: ${result.name}`)\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Effect makes errors explicit in your types:\n\n1. **Errors are typed** - You know exactly what can fail\n2. **Handle or propagate** - Can't accidentally ignore errors\n3. **Recovery options** - Provide fallbacks, retry, or transform\n4. **No try/catch** - Declarative error handling\n\n---", - "content": "## Guideline\n\nHandle errors in Effect using `catchAll` to catch any error, or `catchTag` to handle specific error types.\n\n---\n\n## Rationale\n\nEffect makes errors explicit in your types:\n\n1. **Errors are typed** - You know exactly what can fail\n2. **Handle or propagate** - Can't accidentally ignore errors\n3. **Recovery options** - Provide fallbacks, retry, or transform\n4. **No try/catch** - Declarative error handling\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Data } from \"effect\"\n\n// ============================================\n// 1. Define typed errors\n// ============================================\n\nclass NetworkError extends Data.TaggedError(\"NetworkError\")<{\n readonly url: string\n}> {}\n\nclass NotFoundError extends Data.TaggedError(\"NotFoundError\")<{\n readonly resource: string\n}> {}\n\n// ============================================\n// 2. Functions that can fail\n// ============================================\n\nconst fetchData = (url: string): Effect.Effect =>\n url.startsWith(\"http\")\n ? Effect.succeed(`Data from ${url}`)\n : Effect.fail(new NetworkError({ url }))\n\nconst findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> =>\n id === \"123\"\n ? Effect.succeed({ id, name: \"Alice\" })\n : Effect.fail(new NotFoundError({ resource: `user:${id}` }))\n\n// ============================================\n// 3. Handle ALL errors with catchAll\n// ============================================\n\nconst withFallback = fetchData(\"invalid-url\").pipe(\n Effect.catchAll((error) => {\n console.log(`Failed: ${error.url}, using fallback`)\n return Effect.succeed(\"Fallback data\")\n })\n)\n\n// Result: \"Fallback data\"\n\n// ============================================\n// 4. Handle SPECIFIC errors with catchTag\n// ============================================\n\nconst findUserOrDefault = (id: string) =>\n findUser(id).pipe(\n Effect.catchTag(\"NotFoundError\", (error) => {\n console.log(`User not found: ${error.resource}`)\n return Effect.succeed({ id: \"guest\", name: \"Guest User\" })\n })\n )\n\n// ============================================\n// 5. Handle MULTIPLE error types\n// ============================================\n\nconst fetchUser = (url: string, id: string) =>\n Effect.gen(function* () {\n yield* fetchData(url)\n return yield* findUser(id)\n })\n\nconst robustFetchUser = (url: string, id: string) =>\n fetchUser(url, id).pipe(\n Effect.catchTags({\n NetworkError: (e) => Effect.succeed({ id: \"offline\", name: `Offline (${e.url})` }),\n NotFoundError: (e) => Effect.succeed({ id: \"unknown\", name: `Unknown (${e.resource})` }),\n })\n )\n\n// ============================================\n// 6. Run the examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n // catchAll example\n const data = yield* withFallback\n yield* Effect.log(`Got data: ${data}`)\n\n // catchTag example\n const user = yield* findUserOrDefault(\"999\")\n yield* Effect.log(`Got user: ${user.name}`)\n\n // Multiple error types\n const result = yield* robustFetchUser(\"invalid\", \"999\")\n yield* Effect.log(`Robust result: ${result.name}`)\n})\n\nEffect.runPromise(program)\n```\n\n## Error Handling Options\n\n| Method | Use When |\n|--------|----------|\n| `catchAll` | Handle any error the same way |\n| `catchTag` | Handle one specific error type |\n| `catchTags` | Handle multiple error types differently |\n| `orElse` | Provide an alternative Effect |\n| `option` | Convert to Option (None on error) |\n\n## What Gets Caught\n\n```typescript\n// Only typed errors get caught\nEffect.fail(new MyError()) // ✅ Caught by catchAll/catchTag\n\n// Defects (bugs) are NOT caught\nEffect.die(\"crash\") // ❌ Not caught - propagates up\nthrow new Error() // ❌ Not caught - becomes defect\n```" - }, - { - "id": "http-hello-world", - "title": "Your First HTTP Request", - "description": "Use @effect/platform HttpClient for type-safe HTTP requests with automatic error handling.", - "skillLevel": "beginner", - "useCases": [ - "making-http-requests" - ], - "example": "```typescript\nimport { Effect, Console } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\nimport { NodeHttpClient, NodeRuntime } from \"@effect/platform-node\"\n\n// ============================================\n// 1. Simple GET request\n// ============================================\n\nconst simpleGet = Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n \n // Make a GET request\n const response = yield* client.get(\"https://jsonplaceholder.typicode.com/posts/1\")\n \n // Get response as JSON\n const json = yield* HttpClientResponse.json(response)\n \n return json\n})\n\n// ============================================\n// 2. GET with typed response\n// ============================================\n\ninterface Post {\n id: number\n title: string\n body: string\n userId: number\n}\n\nconst getPost = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/posts/${id}`\n )\n const post = yield* HttpClientResponse.json(response) as Effect.Effect\n return post\n })\n\n// ============================================\n// 3. POST with body\n// ============================================\n\nconst createPost = (title: string, body: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n \n const request = HttpClientRequest.post(\n \"https://jsonplaceholder.typicode.com/posts\"\n ).pipe(\n HttpClientRequest.jsonBody({ title, body, userId: 1 })\n )\n \n const response = yield* client.execute(yield* request)\n const created = yield* HttpClientResponse.json(response)\n \n return created\n })\n\n// ============================================\n// 4. Handle errors\n// ============================================\n\nconst safeGetPost = (id: number) =>\n getPost(id).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Console.error(`Failed to fetch post ${id}: ${error}`)\n return { id, title: \"Unavailable\", body: \"\", userId: 0 }\n })\n )\n )\n\n// ============================================\n// 5. Run the program\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Console.log(\"=== Simple GET ===\")\n const data = yield* simpleGet\n yield* Console.log(JSON.stringify(data, null, 2))\n\n yield* Console.log(\"\\n=== Typed GET ===\")\n const post = yield* getPost(1)\n yield* Console.log(`Post: ${post.title}`)\n\n yield* Console.log(\"\\n=== POST Request ===\")\n const created = yield* createPost(\"My New Post\", \"This is the body\")\n yield* Console.log(`Created: ${JSON.stringify(created)}`)\n})\n\n// Provide the HTTP client implementation\nprogram.pipe(\n Effect.provide(NodeHttpClient.layer),\n NodeRuntime.runMain\n)\n```", - "antiPattern": "", - "explanation": "Effect's HttpClient is better than `fetch`:\n\n1. **Type-safe errors** - Network failures are typed, not exceptions\n2. **Automatic JSON parsing** - No manual `.json()` calls\n3. **Composable** - Chain requests, add retries, timeouts\n4. **Testable** - Easy to mock in tests\n\n---", - "content": "## Guideline\n\nUse Effect's `HttpClient` from `@effect/platform` to make HTTP requests with built-in error handling, retries, and type safety.\n\n---\n\n## Rationale\n\nEffect's HttpClient is better than `fetch`:\n\n1. **Type-safe errors** - Network failures are typed, not exceptions\n2. **Automatic JSON parsing** - No manual `.json()` calls\n3. **Composable** - Chain requests, add retries, timeouts\n4. **Testable** - Easy to mock in tests\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Console } from \"effect\"\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"@effect/platform\"\nimport { NodeHttpClient, NodeRuntime } from \"@effect/platform-node\"\n\n// ============================================\n// 1. Simple GET request\n// ============================================\n\nconst simpleGet = Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n \n // Make a GET request\n const response = yield* client.get(\"https://jsonplaceholder.typicode.com/posts/1\")\n \n // Get response as JSON\n const json = yield* HttpClientResponse.json(response)\n \n return json\n})\n\n// ============================================\n// 2. GET with typed response\n// ============================================\n\ninterface Post {\n id: number\n title: string\n body: string\n userId: number\n}\n\nconst getPost = (id: number) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const response = yield* client.get(\n `https://jsonplaceholder.typicode.com/posts/${id}`\n )\n const post = yield* HttpClientResponse.json(response) as Effect.Effect\n return post\n })\n\n// ============================================\n// 3. POST with body\n// ============================================\n\nconst createPost = (title: string, body: string) =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n \n const request = HttpClientRequest.post(\n \"https://jsonplaceholder.typicode.com/posts\"\n ).pipe(\n HttpClientRequest.jsonBody({ title, body, userId: 1 })\n )\n \n const response = yield* client.execute(yield* request)\n const created = yield* HttpClientResponse.json(response)\n \n return created\n })\n\n// ============================================\n// 4. Handle errors\n// ============================================\n\nconst safeGetPost = (id: number) =>\n getPost(id).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Console.error(`Failed to fetch post ${id}: ${error}`)\n return { id, title: \"Unavailable\", body: \"\", userId: 0 }\n })\n )\n )\n\n// ============================================\n// 5. Run the program\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Console.log(\"=== Simple GET ===\")\n const data = yield* simpleGet\n yield* Console.log(JSON.stringify(data, null, 2))\n\n yield* Console.log(\"\\n=== Typed GET ===\")\n const post = yield* getPost(1)\n yield* Console.log(`Post: ${post.title}`)\n\n yield* Console.log(\"\\n=== POST Request ===\")\n const created = yield* createPost(\"My New Post\", \"This is the body\")\n yield* Console.log(`Created: ${JSON.stringify(created)}`)\n})\n\n// Provide the HTTP client implementation\nprogram.pipe(\n Effect.provide(NodeHttpClient.layer),\n NodeRuntime.runMain\n)\n```\n\n## Request Methods\n\n| Method | Usage |\n|--------|-------|\n| `client.get(url)` | GET request |\n| `client.post(url)` | POST request |\n| `client.put(url)` | PUT request |\n| `client.delete(url)` | DELETE request |\n| `client.execute(request)` | Any request |\n\n## Response Handling\n\n| Function | Purpose |\n|----------|---------|\n| `HttpClientResponse.json` | Parse as JSON |\n| `HttpClientResponse.text` | Get as text |\n| `HttpClientResponse.arrayBuffer` | Get as bytes |\n| `response.status` | HTTP status code |\n| `response.headers` | Response headers |\n\n## Setup\n\n```bash\nbun add @effect/platform @effect/platform-node\n```" - }, - { - "id": "observability-hello-world", - "title": "Your First Logs", - "description": "Use Effect.log and related functions for structured, contextual logging.", - "skillLevel": "beginner", - "useCases": [ - "observability" - ], - "example": "```typescript\nimport { Effect, Logger, LogLevel } from \"effect\"\n\n// ============================================\n// 1. Basic logging\n// ============================================\n\nconst basicLogging = Effect.gen(function* () {\n // Different log levels\n yield* Effect.logDebug(\"Debug message - for development\")\n yield* Effect.logInfo(\"Info message - normal operation\")\n yield* Effect.log(\"Default log - same as logInfo\")\n yield* Effect.logWarning(\"Warning - something unusual\")\n yield* Effect.logError(\"Error - something went wrong\")\n})\n\n// ============================================\n// 2. Logging with context\n// ============================================\n\nconst withContext = Effect.gen(function* () {\n // Add structured data to logs\n yield* Effect.log(\"User logged in\").pipe(\n Effect.annotateLogs({\n userId: \"user-123\",\n action: \"login\",\n ipAddress: \"192.168.1.1\",\n })\n )\n\n // Add a single annotation\n yield* Effect.log(\"Processing request\").pipe(\n Effect.annotateLogs(\"requestId\", \"req-456\")\n )\n})\n\n// ============================================\n// 3. Log spans for timing\n// ============================================\n\nconst withTiming = Effect.gen(function* () {\n yield* Effect.log(\"Starting operation\")\n\n // withLogSpan adds timing information\n yield* Effect.sleep(\"100 millis\").pipe(\n Effect.withLogSpan(\"database-query\")\n )\n\n yield* Effect.log(\"Operation complete\")\n})\n\n// ============================================\n// 4. Practical example\n// ============================================\n\ninterface User {\n id: string\n email: string\n}\n\nconst processOrder = (orderId: string, userId: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Processing order\").pipe(\n Effect.annotateLogs({ orderId, userId })\n )\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\")\n\n yield* Effect.logInfo(\"Order processed successfully\").pipe(\n Effect.annotateLogs({ orderId, status: \"completed\" })\n )\n\n return { orderId, status: \"completed\" }\n }).pipe(\n Effect.withLogSpan(\"processOrder\")\n )\n\n// ============================================\n// 5. Configure log level\n// ============================================\n\nconst debugProgram = basicLogging.pipe(\n // Show all logs including debug\n Logger.withMinimumLogLevel(LogLevel.Debug)\n)\n\nconst productionProgram = basicLogging.pipe(\n // Only show warnings and errors\n Logger.withMinimumLogLevel(LogLevel.Warning)\n)\n\n// ============================================\n// 6. Run\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Basic Logging ===\")\n yield* basicLogging\n\n yield* Effect.log(\"\\n=== With Context ===\")\n yield* withContext\n\n yield* Effect.log(\"\\n=== With Timing ===\")\n yield* withTiming\n\n yield* Effect.log(\"\\n=== Process Order ===\")\n yield* processOrder(\"order-789\", \"user-123\")\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Effect's logging is superior to `console.log`:\n\n1. **Structured** - Logs are data, not just strings\n2. **Contextual** - Automatically includes fiber info, timestamps\n3. **Configurable** - Change log levels, formats, destinations\n4. **Type-safe** - Part of the Effect type system\n\n---", - "content": "## Guideline\n\nUse Effect's built-in logging functions for structured, contextual logging that works with any logging backend.\n\n---\n\n## Rationale\n\nEffect's logging is superior to `console.log`:\n\n1. **Structured** - Logs are data, not just strings\n2. **Contextual** - Automatically includes fiber info, timestamps\n3. **Configurable** - Change log levels, formats, destinations\n4. **Type-safe** - Part of the Effect type system\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Logger, LogLevel } from \"effect\"\n\n// ============================================\n// 1. Basic logging\n// ============================================\n\nconst basicLogging = Effect.gen(function* () {\n // Different log levels\n yield* Effect.logDebug(\"Debug message - for development\")\n yield* Effect.logInfo(\"Info message - normal operation\")\n yield* Effect.log(\"Default log - same as logInfo\")\n yield* Effect.logWarning(\"Warning - something unusual\")\n yield* Effect.logError(\"Error - something went wrong\")\n})\n\n// ============================================\n// 2. Logging with context\n// ============================================\n\nconst withContext = Effect.gen(function* () {\n // Add structured data to logs\n yield* Effect.log(\"User logged in\").pipe(\n Effect.annotateLogs({\n userId: \"user-123\",\n action: \"login\",\n ipAddress: \"192.168.1.1\",\n })\n )\n\n // Add a single annotation\n yield* Effect.log(\"Processing request\").pipe(\n Effect.annotateLogs(\"requestId\", \"req-456\")\n )\n})\n\n// ============================================\n// 3. Log spans for timing\n// ============================================\n\nconst withTiming = Effect.gen(function* () {\n yield* Effect.log(\"Starting operation\")\n\n // withLogSpan adds timing information\n yield* Effect.sleep(\"100 millis\").pipe(\n Effect.withLogSpan(\"database-query\")\n )\n\n yield* Effect.log(\"Operation complete\")\n})\n\n// ============================================\n// 4. Practical example\n// ============================================\n\ninterface User {\n id: string\n email: string\n}\n\nconst processOrder = (orderId: string, userId: string) =>\n Effect.gen(function* () {\n yield* Effect.logInfo(\"Processing order\").pipe(\n Effect.annotateLogs({ orderId, userId })\n )\n\n // Simulate work\n yield* Effect.sleep(\"50 millis\")\n\n yield* Effect.logInfo(\"Order processed successfully\").pipe(\n Effect.annotateLogs({ orderId, status: \"completed\" })\n )\n\n return { orderId, status: \"completed\" }\n }).pipe(\n Effect.withLogSpan(\"processOrder\")\n )\n\n// ============================================\n// 5. Configure log level\n// ============================================\n\nconst debugProgram = basicLogging.pipe(\n // Show all logs including debug\n Logger.withMinimumLogLevel(LogLevel.Debug)\n)\n\nconst productionProgram = basicLogging.pipe(\n // Only show warnings and errors\n Logger.withMinimumLogLevel(LogLevel.Warning)\n)\n\n// ============================================\n// 6. Run\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"=== Basic Logging ===\")\n yield* basicLogging\n\n yield* Effect.log(\"\\n=== With Context ===\")\n yield* withContext\n\n yield* Effect.log(\"\\n=== With Timing ===\")\n yield* withTiming\n\n yield* Effect.log(\"\\n=== Process Order ===\")\n yield* processOrder(\"order-789\", \"user-123\")\n})\n\nEffect.runPromise(program)\n```\n\n## Log Levels\n\n| Level | Function | Use For |\n|-------|----------|---------|\n| Debug | `Effect.logDebug` | Development details |\n| Info | `Effect.logInfo` / `Effect.log` | Normal operations |\n| Warning | `Effect.logWarning` | Unusual but handled |\n| Error | `Effect.logError` | Failures and problems |\n| Fatal | `Effect.logFatal` | Unrecoverable errors |\n\n## Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `Effect.log(message)` | Log at info level |\n| `Effect.annotateLogs(data)` | Add structured context |\n| `Effect.withLogSpan(name)` | Add timing information |\n| `Logger.withMinimumLogLevel` | Filter by log level |" - }, - { - "id": "concurrency-hello-world", - "title": "Your First Parallel Operation", - "description": "Use Effect.all with concurrency option to run independent effects in parallel.", - "skillLevel": "beginner", - "useCases": [ - "concurrency-getting-started" - ], - "example": "```typescript\nimport { Effect } from \"effect\"\n\n// Simulate async operations\nconst fetchUser = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return { id: 1, name: \"Alice\" }\n})\n\nconst fetchProducts = Effect.gen(function* () {\n yield* Effect.sleep(\"150 millis\")\n return [{ id: 1, name: \"Widget\" }, { id: 2, name: \"Gadget\" }]\n})\n\nconst fetchCart = Effect.gen(function* () {\n yield* Effect.sleep(\"80 millis\")\n return { items: 3, total: 99.99 }\n})\n\n// ============================================\n// SEQUENTIAL: One after another (~330ms)\n// ============================================\n\nconst sequential = Effect.all([fetchUser, fetchProducts, fetchCart])\n\n// ============================================\n// PARALLEL: All at once (~150ms)\n// ============================================\n\nconst parallel = Effect.all(\n [fetchUser, fetchProducts, fetchCart],\n { concurrency: \"unbounded\" }\n)\n\n// ============================================\n// PARALLEL WITH LIMIT: Max 2 at a time\n// ============================================\n\nconst limited = Effect.all(\n [fetchUser, fetchProducts, fetchCart],\n { concurrency: 2 }\n)\n\n// ============================================\n// DEMO\n// ============================================\n\nconst demo = Effect.gen(function* () {\n const start = Date.now()\n \n const [user, products, cart] = yield* parallel\n \n const elapsed = Date.now() - start\n yield* Effect.log(`Fetched in ${elapsed}ms`)\n yield* Effect.log(`User: ${user.name}`)\n yield* Effect.log(`Products: ${products.length}`)\n yield* Effect.log(`Cart total: $${cart.total}`)\n})\n\nEffect.runPromise(demo)\n// Output: Fetched in ~150ms (not ~330ms!)\n```", - "antiPattern": "", - "explanation": "Parallel execution speeds up independent operations:\n\n1. **Fetch multiple APIs** - Get user, products, cart simultaneously\n2. **Process files** - Read multiple files at once\n3. **Database queries** - Run independent queries in parallel\n\n---", - "content": "## Guideline\n\nUse `Effect.all` with `{ concurrency: \"unbounded\" }` to run independent effects in parallel. Without the option, effects run sequentially.\n\n---\n\n## Rationale\n\nParallel execution speeds up independent operations:\n\n1. **Fetch multiple APIs** - Get user, products, cart simultaneously\n2. **Process files** - Read multiple files at once\n3. **Database queries** - Run independent queries in parallel\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\"\n\n// Simulate async operations\nconst fetchUser = Effect.gen(function* () {\n yield* Effect.sleep(\"100 millis\")\n return { id: 1, name: \"Alice\" }\n})\n\nconst fetchProducts = Effect.gen(function* () {\n yield* Effect.sleep(\"150 millis\")\n return [{ id: 1, name: \"Widget\" }, { id: 2, name: \"Gadget\" }]\n})\n\nconst fetchCart = Effect.gen(function* () {\n yield* Effect.sleep(\"80 millis\")\n return { items: 3, total: 99.99 }\n})\n\n// ============================================\n// SEQUENTIAL: One after another (~330ms)\n// ============================================\n\nconst sequential = Effect.all([fetchUser, fetchProducts, fetchCart])\n\n// ============================================\n// PARALLEL: All at once (~150ms)\n// ============================================\n\nconst parallel = Effect.all(\n [fetchUser, fetchProducts, fetchCart],\n { concurrency: \"unbounded\" }\n)\n\n// ============================================\n// PARALLEL WITH LIMIT: Max 2 at a time\n// ============================================\n\nconst limited = Effect.all(\n [fetchUser, fetchProducts, fetchCart],\n { concurrency: 2 }\n)\n\n// ============================================\n// DEMO\n// ============================================\n\nconst demo = Effect.gen(function* () {\n const start = Date.now()\n \n const [user, products, cart] = yield* parallel\n \n const elapsed = Date.now() - start\n yield* Effect.log(`Fetched in ${elapsed}ms`)\n yield* Effect.log(`User: ${user.name}`)\n yield* Effect.log(`Products: ${products.length}`)\n yield* Effect.log(`Cart total: $${cart.total}`)\n})\n\nEffect.runPromise(demo)\n// Output: Fetched in ~150ms (not ~330ms!)\n```\n\n## Concurrency Options\n\n| Option | Behavior |\n|--------|----------|\n| **(none)** | Sequential, one at a time |\n| `{ concurrency: \"unbounded\" }` | All in parallel |\n| `{ concurrency: N }` | Max N in parallel |\n| `{ concurrency: \"inherit\" }` | Use parent's setting |\n\n## When to Use Parallel\n\n✅ **Use parallel when:**\n- Operations are independent\n- No shared mutable state\n- Speed matters\n\n❌ **Use sequential when:**\n- Operations depend on each other\n- Order matters\n- Rate limiting required\n\n## Quick Reference\n\n```typescript\n// Sequential (default)\nEffect.all([a, b, c])\n\n// Parallel (all at once)\nEffect.all([a, b, c], { concurrency: \"unbounded\" })\n\n// Limited parallel (max 5)\nEffect.all([a, b, c], { concurrency: 5 })\n```" - }, - { - "id": "platform-hello-world", - "title": "Your First Platform Operation", - "description": "Use @effect/platform for cross-platform system operations with Effect integration.", - "skillLevel": "beginner", - "useCases": [ - "platform-getting-started" - ], - "example": "```typescript\nimport { Effect } from \"effect\"\nimport { FileSystem } from \"@effect/platform\"\nimport { NodeContext, NodeRuntime } from \"@effect/platform-node\"\n\n// Read a file - returns Effect\nconst readConfig = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n \n // Read file as UTF-8 string\n const content = yield* fs.readFileString(\"./config.json\")\n \n return JSON.parse(content)\n})\n\n// Write a file\nconst writeLog = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n \n yield* fs.writeFileString(\n \"./app.log\",\n `Started at ${new Date().toISOString()}\\n`\n )\n})\n\n// Combine operations\nconst program = Effect.gen(function* () {\n const config = yield* readConfig\n yield* Effect.log(`Loaded config: ${config.appName}`)\n \n yield* writeLog\n yield* Effect.log(\"Log file created\")\n})\n\n// Run with Node.js platform\nprogram.pipe(\n Effect.provide(NodeContext.layer),\n NodeRuntime.runMain\n)\n```", - "antiPattern": "", - "explanation": "Platform wraps system operations in Effect, giving you:\n\n1. **Type safety** - File operations return `Effect`\n2. **Resource management** - Files are automatically closed\n3. **Cross-platform** - Same code works on Node.js, Bun, browser\n4. **Composability** - Chain file ops with other effects\n\n---", - "content": "## Guideline\n\nEffect Platform provides type-safe, cross-platform system operations. Use `@effect/platform-node` for Node.js or `@effect/platform-bun` for Bun.\n\n---\n\n## Rationale\n\nPlatform wraps system operations in Effect, giving you:\n\n1. **Type safety** - File operations return `Effect`\n2. **Resource management** - Files are automatically closed\n3. **Cross-platform** - Same code works on Node.js, Bun, browser\n4. **Composability** - Chain file ops with other effects\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect } from \"effect\"\nimport { FileSystem } from \"@effect/platform\"\nimport { NodeContext, NodeRuntime } from \"@effect/platform-node\"\n\n// Read a file - returns Effect\nconst readConfig = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n \n // Read file as UTF-8 string\n const content = yield* fs.readFileString(\"./config.json\")\n \n return JSON.parse(content)\n})\n\n// Write a file\nconst writeLog = Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem\n \n yield* fs.writeFileString(\n \"./app.log\",\n `Started at ${new Date().toISOString()}\\n`\n )\n})\n\n// Combine operations\nconst program = Effect.gen(function* () {\n const config = yield* readConfig\n yield* Effect.log(`Loaded config: ${config.appName}`)\n \n yield* writeLog\n yield* Effect.log(\"Log file created\")\n})\n\n// Run with Node.js platform\nprogram.pipe(\n Effect.provide(NodeContext.layer),\n NodeRuntime.runMain\n)\n```\n\n## Platform vs Node.js\n\n| Aspect | Node.js fs | Effect Platform |\n|--------|-----------|-----------------|\n| Error handling | try/catch or callbacks | Typed `Effect` |\n| Resource cleanup | Manual close() | Automatic |\n| Async model | Promises/callbacks | Effect with fibers |\n| Cross-platform | Node.js only | Node, Bun, Browser |\n\n## Key Services\n\n| Service | Purpose |\n|---------|---------|\n| **FileSystem** | Read, write, delete files |\n| **Path** | Cross-platform path manipulation |\n| **Command** | Execute shell commands |\n| **Terminal** | Interactive I/O |\n| **KeyValueStore** | Persistent storage |" - }, - { - "id": "scheduling-hello-world", - "title": "Your First Schedule", - "description": "Use Schedule to control when and how often effects run.", - "skillLevel": "beginner", - "useCases": [ - "scheduling" - ], - "example": "```typescript\nimport { Effect, Schedule } from \"effect\"\n\n// ============================================\n// 1. Retry a failing operation\n// ============================================\n\nlet attempts = 0\nconst flakyOperation = Effect.gen(function* () {\n attempts++\n if (attempts < 3) {\n yield* Effect.log(`Attempt ${attempts} failed`)\n return yield* Effect.fail(new Error(\"Temporary failure\"))\n }\n return `Success on attempt ${attempts}`\n})\n\n// Retry up to 5 times\nconst withRetry = flakyOperation.pipe(\n Effect.retry(Schedule.recurs(5))\n)\n\n// ============================================\n// 2. Repeat a successful operation\n// ============================================\n\nconst logTime = Effect.gen(function* () {\n const now = new Date().toISOString()\n yield* Effect.log(`Current time: ${now}`)\n return now\n})\n\n// Repeat 3 times\nconst repeated = logTime.pipe(\n Effect.repeat(Schedule.recurs(3))\n)\n\n// ============================================\n// 3. Add delays between operations\n// ============================================\n\n// Repeat every second, 5 times\nconst polling = logTime.pipe(\n Effect.repeat(\n Schedule.spaced(\"1 second\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n )\n )\n)\n\n// ============================================\n// 4. Common schedule patterns\n// ============================================\n\n// Fixed delay between attempts\nconst fixedDelay = Schedule.spaced(\"500 millis\")\n\n// Increasing delay (1s, 2s, 4s, 8s...)\nconst exponentialBackoff = Schedule.exponential(\"1 second\")\n\n// Maximum number of attempts\nconst limitedAttempts = Schedule.recurs(3)\n\n// Combine: exponential backoff, max 5 attempts\nconst retryPolicy = Schedule.exponential(\"100 millis\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n)\n\n// ============================================\n// 5. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"--- Retry Example ---\")\n const result = yield* withRetry\n yield* Effect.log(`Result: ${result}`)\n\n yield* Effect.log(\"\\n--- Repeat Example ---\")\n yield* repeated\n})\n\nEffect.runPromise(program)\n```", - "antiPattern": "", - "explanation": "Schedules solve common timing problems:\n\n1. **Retries** - Try again after failures\n2. **Polling** - Check for updates periodically\n3. **Rate limiting** - Control how fast things run\n4. **Backoff** - Increase delays between attempts\n\n---", - "content": "## Guideline\n\nUse `Schedule` to control timing in Effect programs - retrying failed operations, repeating successful ones, or adding delays.\n\n---\n\n## Rationale\n\nSchedules solve common timing problems:\n\n1. **Retries** - Try again after failures\n2. **Polling** - Check for updates periodically\n3. **Rate limiting** - Control how fast things run\n4. **Backoff** - Increase delays between attempts\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Schedule } from \"effect\"\n\n// ============================================\n// 1. Retry a failing operation\n// ============================================\n\nlet attempts = 0\nconst flakyOperation = Effect.gen(function* () {\n attempts++\n if (attempts < 3) {\n yield* Effect.log(`Attempt ${attempts} failed`)\n return yield* Effect.fail(new Error(\"Temporary failure\"))\n }\n return `Success on attempt ${attempts}`\n})\n\n// Retry up to 5 times\nconst withRetry = flakyOperation.pipe(\n Effect.retry(Schedule.recurs(5))\n)\n\n// ============================================\n// 2. Repeat a successful operation\n// ============================================\n\nconst logTime = Effect.gen(function* () {\n const now = new Date().toISOString()\n yield* Effect.log(`Current time: ${now}`)\n return now\n})\n\n// Repeat 3 times\nconst repeated = logTime.pipe(\n Effect.repeat(Schedule.recurs(3))\n)\n\n// ============================================\n// 3. Add delays between operations\n// ============================================\n\n// Repeat every second, 5 times\nconst polling = logTime.pipe(\n Effect.repeat(\n Schedule.spaced(\"1 second\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n )\n )\n)\n\n// ============================================\n// 4. Common schedule patterns\n// ============================================\n\n// Fixed delay between attempts\nconst fixedDelay = Schedule.spaced(\"500 millis\")\n\n// Increasing delay (1s, 2s, 4s, 8s...)\nconst exponentialBackoff = Schedule.exponential(\"1 second\")\n\n// Maximum number of attempts\nconst limitedAttempts = Schedule.recurs(3)\n\n// Combine: exponential backoff, max 5 attempts\nconst retryPolicy = Schedule.exponential(\"100 millis\").pipe(\n Schedule.intersect(Schedule.recurs(5))\n)\n\n// ============================================\n// 5. Run examples\n// ============================================\n\nconst program = Effect.gen(function* () {\n yield* Effect.log(\"--- Retry Example ---\")\n const result = yield* withRetry\n yield* Effect.log(`Result: ${result}`)\n\n yield* Effect.log(\"\\n--- Repeat Example ---\")\n yield* repeated\n})\n\nEffect.runPromise(program)\n```\n\n## Common Schedules\n\n| Schedule | What It Does |\n|----------|--------------|\n| `Schedule.recurs(n)` | Run at most n times |\n| `Schedule.spaced(duration)` | Wait duration between runs |\n| `Schedule.exponential(base)` | Double the delay each time |\n| `Schedule.forever` | Run indefinitely |\n| `Schedule.once` | Run exactly once |\n\n## Combining Schedules\n\n| Combinator | Meaning |\n|------------|---------|\n| `intersect` | Both conditions must be true |\n| `union` | Either condition can be true |\n| `andThen` | First schedule, then second |\n\n## When to Use\n\n- **Retries** - Network requests, database connections\n- **Polling** - Check for job completion\n- **Rate limiting** - API call quotas\n- **Periodic tasks** - Health checks, cache refresh" - }, - { - "id": "stream-hello-world", - "title": "Your First Stream", - "description": "Use Stream to process sequences of data lazily and efficiently.", - "skillLevel": "beginner", - "useCases": [ - "streams-getting-started" - ], - "example": "```typescript\nimport { Effect, Stream } from \"effect\"\n\n// Create a stream from explicit values\nconst numbers = Stream.make(1, 2, 3, 4, 5)\n\n// Create a stream from an array\nconst fromArray = Stream.fromIterable([10, 20, 30])\n\n// Create a single-value stream\nconst single = Stream.succeed(\"hello\")\n\n// Transform and run the stream\nconst program = numbers.pipe(\n Stream.map((n) => n * 2), // Double each number\n Stream.filter((n) => n > 4), // Keep only > 4\n Stream.runCollect // Collect results\n)\n\nEffect.runPromise(program).then((chunk) => {\n console.log([...chunk]) // [6, 8, 10]\n})\n```", - "antiPattern": "Don't use regular arrays when you need lazy processing or async operations:\n\n```typescript\n// Anti-pattern: Eager processing, all in memory\nconst numbers = [1, 2, 3, 4, 5]\nconst doubled = numbers.map((n) => n * 2)\nconst filtered = doubled.filter((n) => n > 4)\n```\n\nThis loads everything into memory immediately. Use Stream when:\n- Data is large or potentially infinite\n- Data arrives asynchronously\n- You need backpressure or resource management", - "explanation": "Streams are Effect's answer to processing sequences of data. Unlike arrays which hold all values in memory at once, streams produce values on demand. This makes them ideal for:\n\n1. **Large datasets** - Process millions of records without loading everything into memory\n2. **Async data** - Handle data that arrives over time (files, APIs, events)\n3. **Composable pipelines** - Chain transformations that work element by element\n\n---", - "content": "## Guideline\n\nA Stream is a lazy sequence of values that can be processed one at a time. Create streams with `Stream.make`, `Stream.fromIterable`, or `Stream.succeed`.\n\n---\n\n## Rationale\n\nStreams are Effect's answer to processing sequences of data. Unlike arrays which hold all values in memory at once, streams produce values on demand. This makes them ideal for:\n\n1. **Large datasets** - Process millions of records without loading everything into memory\n2. **Async data** - Handle data that arrives over time (files, APIs, events)\n3. **Composable pipelines** - Chain transformations that work element by element\n\n---\n\n## Good Example\n\n```typescript\nimport { Effect, Stream } from \"effect\"\n\n// Create a stream from explicit values\nconst numbers = Stream.make(1, 2, 3, 4, 5)\n\n// Create a stream from an array\nconst fromArray = Stream.fromIterable([10, 20, 30])\n\n// Create a single-value stream\nconst single = Stream.succeed(\"hello\")\n\n// Transform and run the stream\nconst program = numbers.pipe(\n Stream.map((n) => n * 2), // Double each number\n Stream.filter((n) => n > 4), // Keep only > 4\n Stream.runCollect // Collect results\n)\n\nEffect.runPromise(program).then((chunk) => {\n console.log([...chunk]) // [6, 8, 10]\n})\n```\n\n## Key Concepts\n\n| Concept | Explanation |\n|---------|-------------|\n| **Stream.make** | Create stream from explicit values |\n| **Stream.fromIterable** | Create stream from array/iterable |\n| **Stream.succeed** | Single-value stream |\n| **Stream.map** | Transform each element |\n| **Stream.filter** | Keep elements matching condition |\n| **Stream.runCollect** | Run stream, collect all results |\n\n## Anti-Pattern\n\nDon't use regular arrays when you need lazy processing or async operations:\n\n```typescript\n// Anti-pattern: Eager processing, all in memory\nconst numbers = [1, 2, 3, 4, 5]\nconst doubled = numbers.map((n) => n * 2)\nconst filtered = doubled.filter((n) => n > 4)\n```\n\nThis loads everything into memory immediately. Use Stream when:\n- Data is large or potentially infinite\n- Data arrives asynchronously\n- You need backpressure or resource management" + "example": "console.log('test');", + "content": "Full content here" } ] \ No newline at end of file diff --git a/content/published/rules/rules.md b/content/published/rules/rules.md index 7a3c5a4b..55cb0f7b 100644 --- a/content/published/rules/rules.md +++ b/content/published/rules/rules.md @@ -1,28684 +1,12 @@ # Effect-TS Patterns - Complete Rules +## Test Rule +**Rule:** A test rule description +**Skill Level:** Beginner +**Use Cases:** Testing -## Access Configuration from the Context +### Example -**Rule:** Access configuration from the Effect context. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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. - -### Anti-Pattern - -Passing configuration values down through multiple function arguments ("prop-drilling"). This is cumbersome and obscures which components truly need which values. - -### Explanation - -This allows your business logic to declaratively state its dependency on a piece of configuration. The logic is clean, type-safe, and completely decoupled from _how_ the configuration is provided. - ---- - -## Access Environment Variables - -**Rule:** Use Effect to access environment variables with proper error handling. - -**Skill Level:** beginner - -**Use Cases:** platform-getting-started - -### Good Example - -```typescript -import { Effect, Config, Option } from "effect" - -// ============================================ -// BASIC: Read required variable -// ============================================ - -const getApiKey = Config.string("API_KEY") - -const program1 = Effect.gen(function* () { - const apiKey = yield* getApiKey - yield* Effect.log(`API Key: ${apiKey.slice(0, 4)}...`) -}) - -// ============================================ -// OPTIONAL: With default value -// ============================================ - -const getPort = Config.number("PORT").pipe( - Config.withDefault(3000) -) - -const program2 = Effect.gen(function* () { - const port = yield* getPort - yield* Effect.log(`Server will run on port ${port}`) -}) - -// ============================================ -// OPTIONAL: Return Option instead of failing -// ============================================ - -const getOptionalFeature = Config.string("FEATURE_FLAG").pipe( - Config.option -) - -const program3 = Effect.gen(function* () { - const feature = yield* getOptionalFeature - - if (Option.isSome(feature)) { - yield* Effect.log(`Feature enabled: ${feature.value}`) - } else { - yield* Effect.log("Feature flag not set") - } -}) - -// ============================================ -// COMBINED: Multiple variables as config object -// ============================================ - -const AppConfig = Config.all({ - apiKey: Config.string("API_KEY"), - apiUrl: Config.string("API_URL"), - port: Config.number("PORT").pipe(Config.withDefault(3000)), - debug: Config.boolean("DEBUG").pipe(Config.withDefault(false)), -}) - -const program4 = Effect.gen(function* () { - const config = yield* AppConfig - - yield* Effect.log(`API URL: ${config.apiUrl}`) - yield* Effect.log(`Port: ${config.port}`) - yield* Effect.log(`Debug: ${config.debug}`) -}) - -// ============================================ -// RUN: Will fail if required vars missing -// ============================================ - -Effect.runPromise(program4).catch((error) => { - console.error("Missing required environment variables") - console.error(error) -}) -``` - -### Explanation - -Environment variables can be missing or malformed. Effect helps you: - -1. **Handle missing vars** - Return `Option` or fail with typed error -2. **Validate values** - Parse and validate with Schema -3. **Provide defaults** - Fallback values when vars are missing -4. **Document requirements** - Types show what's needed - ---- - ---- - -## Accessing the Current Time with Clock - -**Rule:** Use the Clock service to get the current time, enabling deterministic testing with TestClock. - -**Skill Level:** intermediate - -**Use Cases:** testing - -### Good 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) => - Clock.currentTimeMillis.pipe( - Effect.flatMap((currentTime) => - Effect.log( - `Token expired? ${expired} (current time: ${new Date(currentTime).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()))) -); -``` - ---- - -### Anti-Pattern - -Directly calling `Date.now()` inside your business logic. This creates an impure function that cannot be tested reliably without manipulating the system clock, which is a bad practice. - -```typescript -import { Effect } from "effect"; - -interface Token { - readonly expiresAt: number; -} - -// ❌ WRONG: This function's behavior changes every millisecond. -const isTokenExpiredUnsafely = (token: Token): Effect.Effect => - Effect.sync(() => Date.now() > token.expiresAt); - -// Testing this function would require complex mocking of global APIs -// or would be non-deterministic. -``` - -### Explanation - -Directly calling `Date.now()` makes your code impure and tightly coupled to the system clock. This makes testing difficult and unreliable, as the output of your function will change every time it's run. - -The `Clock` service is Effect's solution to this problem. It's an abstraction for "the current time." - -- In **production**, the default `Live` `Clock` implementation uses the real system time. -- In **tests**, you can provide the `TestClock` layer. This gives you a virtual clock that you can manually control, allowing you to set the time to a specific value or advance it by a specific duration. - -This makes any time-dependent logic pure, deterministic, and easy to test with perfect precision. - ---- - ---- - -## Accumulate Multiple Errors with Either - -**Rule:** Use Either to accumulate multiple validation errors instead of failing on the first one. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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, Either } 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* () { - yield* Effect.log("Validating users...\n"); - - for (const input of invalidInputs) { - const result = yield* Effect.either(validateUser(input)); - - yield* Effect.log(`Validating user: ${input.name} <${input.email}>`); - - // Handle success and failure cases separately for clarity - // Using Either.match which is the idiomatic way to handle Either values - yield* Either.match(result, { - onLeft: (error) => - Effect.gen(function* () { - yield* Effect.log("❌ Validation failed:"); - yield* Effect.log(error.message); - yield* Effect.log(""); // Empty line for readability - }), - onRight: (user) => - Effect.gen(function* () { - yield* Effect.log(`✅ User is valid: ${JSON.stringify(user)}`); - yield* Effect.log(""); // Empty line for readability - }), - }); - } -}); - -// Run the program -Effect.runSync(program); -``` - ---- - -### Anti-Pattern - -Using `Effect`'s error channel for validation that requires multiple error messages. The code below will only ever report the first error it finds, because `Effect.fail` short-circuits the entire `Effect.gen` block. - -```typescript -import { Effect } from "effect"; - -const validateWithEffect = (input: { name: string; email: string }) => - Effect.gen(function* () { - if (input.name.length < 3) { - // The program will fail here and never check the email. - return yield* Effect.fail("Name is too short."); - } - if (!input.email.includes("@")) { - return yield* Effect.fail("Email is invalid."); - } - return yield* Effect.succeed(input); - }); -``` - -### Explanation - -The `Effect` error channel is designed to short-circuit. The moment an `Effect` fails, the entire computation stops and the error is propagated. This is perfect for handling unrecoverable errors like a lost database connection. - -However, for tasks like validating a user's input, this is poor user experience. You want to show the user all of their mistakes at once. - -`Either` is the solution. Since it's a pure data structure, you can run multiple checks that each return an `Either`, and then combine the results to accumulate all the `Left` (error) values. The `Effect/Schema` module uses this pattern internally to provide powerful error accumulation. - ---- - ---- - -## Accumulate Multiple Errors with Either - -**Rule:** Use Either to model computations that may fail, making errors explicit and type-safe. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Either } from "effect"; - -// Create a Right (success) or Left (failure) -const success = Either.right(42); // Either -const failure = Either.left("Something went wrong"); // Either - -// Pattern match on Either -const result = success.pipe( - Either.match({ - onLeft: (err) => `Error: ${err}`, - onRight: (value) => `Value: ${value}`, - }) -); // string - -// Combine multiple Eithers and accumulate errors -const e1 = Either.right(1); -const e2 = Either.left("fail1"); -const e3 = Either.left("fail2"); - -const all = [e1, e2, e3].filter(Either.isRight).map(Either.getRight); // [1] -const errors = [e1, e2, e3].filter(Either.isLeft).map(Either.getLeft); // ["fail1", "fail2"] -``` - -**Explanation:** - -- `Either.right(value)` represents success. -- `Either.left(error)` represents failure. -- Pattern matching ensures all cases are handled. -- You can accumulate errors or results from multiple Eithers. - -### Anti-Pattern - -Throwing exceptions or using ad-hoc error codes, which are not type-safe, not composable, and make error handling less predictable. - -### Explanation - -`Either` is a foundational data type for error handling in functional programming. -It allows you to accumulate errors, model domain-specific failures, and avoid exceptions and unchecked errors. - ---- - -## Add Caching by Wrapping a Layer - -**Rule:** Use a wrapping Layer to add cross-cutting concerns like caching to a service without altering its original implementation. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -We have a `WeatherService` that makes slow API calls. We create a `WeatherService.cached` wrapper layer that adds an in-memory cache using a `Ref` and a `Map`. - -```typescript -import { Effect, Layer, Ref } from "effect"; - -// 1. Define the service interface -class WeatherService extends Effect.Service()( - "WeatherService", - { - sync: () => ({ - getForecast: (city: string) => Effect.succeed(`Sunny in ${city}`), - }), - } -) {} - -// 2. The "Live" implementation that is slow -const WeatherServiceLive = Layer.succeed( - WeatherService, - WeatherService.of({ - _tag: "WeatherService", - getForecast: (city) => - Effect.succeed(`Sunny in ${city}`).pipe( - Effect.delay("2 seconds"), - Effect.tap(() => Effect.log(`Fetched live forecast for ${city}`)) - ), - }) -); - -// 3. The Caching Wrapper Layer -const WeatherServiceCached = Layer.effect( - WeatherService, - Effect.gen(function* () { - // It REQUIRES the original WeatherService - const underlyingService = yield* WeatherService; - const cache = yield* Ref.make(new Map()); - - return WeatherService.of({ - _tag: "WeatherService", - getForecast: (city) => - Ref.get(cache).pipe( - Effect.flatMap((map) => - map.has(city) - ? Effect.log(`Cache HIT for ${city}`).pipe( - Effect.as(map.get(city)!) - ) - : Effect.log(`Cache MISS for ${city}`).pipe( - Effect.flatMap(() => underlyingService.getForecast(city)), - Effect.tap((forecast) => - Ref.update(cache, (map) => map.set(city, forecast)) - ) - ) - ) - ), - }); - }) -); - -// 4. Compose the final layer. The wrapper is provided with the live implementation. -const AppLayer = Layer.provide(WeatherServiceCached, WeatherServiceLive); - -// 5. The application logic -const program = Effect.gen(function* () { - const weather = yield* WeatherService; - yield* weather.getForecast("London"); // First call is slow (MISS) - yield* weather.getForecast("London"); // Second call is instant (HIT) -}); - -Effect.runPromise(Effect.provide(program, AppLayer)); -``` - ---- - -### Anti-Pattern - -Modifying the original service implementation to include caching logic directly. This violates the Single Responsibility Principle by mixing the core logic of fetching weather with the cross-cutting concern of caching. - -```typescript -// ❌ WRONG: The service is now responsible for both its logic and its caching strategy. -const WeatherServiceWithInlineCache = Layer.effect( - WeatherService, - Effect.gen(function* () { - const cache = yield* Ref.make(new Map()); - return WeatherService.of({ - getForecast: (city) => { - // ...caching logic mixed directly with fetching logic... - return Effect.succeed("..."); - }, - }); - }) -); -``` - -### Explanation - -You often want to add functionality like caching, logging, or metrics to a service without polluting its core business logic. The wrapper layer pattern is a clean way to achieve this. - -By creating a layer that _requires_ the original service, you can get an instance of it from the context, and then provide a _new_ implementation of that same service that calls the original. - -This approach is powerful because: - -- **It's Non-Invasive:** The original service (`DatabaseLive`) remains completely unchanged. -- **It's Composable:** You can apply multiple wrappers. You could wrap a database layer with a caching layer, then wrap that with a metrics layer. -- **It's Explicit:** The composition is clearly defined at the application's top level where you build your final `AppLayer`. - ---- - ---- - -## Add Custom Metrics to Your Application - -**Rule:** Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const result = yield* createUser; - yield* Effect.log(`Result: ${JSON.stringify(result)}`); - return result; -}); - -Effect.runPromise(programWithLogging); -``` - ---- - -### Anti-Pattern - -Not adding any metrics to your application. Without metrics, you are flying blind. You have no high-level overview of your application's health, performance, or business KPIs. You can't build dashboards, you can't set up alerts for abnormal behavior (e.g., "error rate is too high"), and you are forced to rely on digging through logs to -understand the state of your system. - -### Explanation - -While logs are for events and traces are for requests, metrics are for aggregation. They provide a high-level, numerical view of your system's health over time, which is perfect for building dashboards and setting up alerts. - -Effect's `Metric` module provides a simple, declarative way to add this instrumentation. By defining your metrics upfront, you can then use operators like `Metric.increment` or `Effect.timed` to update them. This is fully integrated with Effect's context system, allowing you to provide different metric backends (like Prometheus or StatsD) via a `Layer`. - -This allows you to answer questions like: - -- "What is our user sign-up rate over the last 24 hours?" -- "Are we approaching our maximum number of database connections?" -- "What is the 95th percentile latency for our API requests?" - ---- - ---- - -## Add Custom Metrics to Your Application - -**Rule:** Use Effect's Metric module to define and update custom metrics for business and performance monitoring. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, Metric } from "effect"; - -// Define a counter metric for processed jobs -const jobsProcessed = Metric.counter("jobs_processed"); - -// Increment the counter when a job is processed -const processJob = Effect.gen(function* () { - // ... process the job - yield* Effect.log("Job processed"); - yield* Metric.increment(jobsProcessed); -}); - -// Define a gauge for current active users -const activeUsers = Metric.gauge("active_users"); - -// Update the gauge when users sign in or out -const userSignedIn = Metric.set(activeUsers, 1); // Set to 1 (simplified example) -const userSignedOut = Metric.set(activeUsers, 0); // Set to 0 (simplified example) - -// Define a histogram for request durations -const requestDuration = Metric.histogram("request_duration", [ - 0.1, 0.5, 1, 2, 5, -] as any); // boundaries in seconds - -// Record a request duration -const recordDuration = (duration: number) => - Metric.update(requestDuration, duration); -``` - -**Explanation:** - -- `Metric.counter` tracks counts of events. -- `Metric.gauge` tracks a value that can go up or down (e.g., active users). -- `Metric.histogram` tracks distributions (e.g., request durations). -- `Effect.updateMetric` updates the metric in your workflow. - -### Anti-Pattern - -Relying solely on logs for monitoring, or using ad-hoc counters and variables that are not integrated with your observability stack. - -### Explanation - -Metrics provide quantitative insight into your application's behavior and performance. -By instrumenting your code with metrics, you can monitor key events, detect anomalies, and drive business decisions. - ---- - -## Add Rate Limiting to APIs - -**Rule:** Use a rate limiter service to enforce request quotas per client. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good Example - -```typescript -import { Effect, Context, Layer, Ref, HashMap, Data, Duration } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. Define rate limit types -// ============================================ - -interface RateLimitConfig { - readonly maxRequests: number - readonly windowMs: number -} - -interface RateLimitState { - readonly count: number - readonly resetAt: number -} - -class RateLimitExceededError extends Data.TaggedError("RateLimitExceededError")<{ - readonly retryAfter: number - readonly limit: number -}> {} - -// ============================================ -// 2. Rate limiter service -// ============================================ - -interface RateLimiter { - readonly check: (key: string) => Effect.Effect - readonly getStatus: (key: string) => Effect.Effect<{ - remaining: number - resetAt: number - }> -} - -class RateLimiterService extends Context.Tag("RateLimiter")< - RateLimiterService, - RateLimiter ->() {} - -// ============================================ -// 3. In-memory rate limiter implementation -// ============================================ - -const makeRateLimiter = (config: RateLimitConfig) => - Effect.gen(function* () { - const state = yield* Ref.make(HashMap.empty()) - - const getOrCreateState = (key: string, now: number) => - Ref.modify(state, (map) => { - const existing = HashMap.get(map, key) - - if (existing._tag === "Some") { - // Check if window expired - if (now >= existing.value.resetAt) { - // Start new window - const newState: RateLimitState = { - count: 0, - resetAt: now + config.windowMs, - } - return [newState, HashMap.set(map, key, newState)] - } - return [existing.value, map] - } - - // Create new entry - const newState: RateLimitState = { - count: 0, - resetAt: now + config.windowMs, - } - return [newState, HashMap.set(map, key, newState)] - }) - - const incrementCount = (key: string) => - Ref.modify(state, (map) => { - const existing = HashMap.get(map, key) - if (existing._tag === "Some") { - const updated = { ...existing.value, count: existing.value.count + 1 } - return [updated.count, HashMap.set(map, key, updated)] - } - return [1, map] - }) - - const limiter: RateLimiter = { - check: (key) => - Effect.gen(function* () { - const now = Date.now() - const currentState = yield* getOrCreateState(key, now) - - if (currentState.count >= config.maxRequests) { - const retryAfter = Math.ceil((currentState.resetAt - now) / 1000) - return yield* Effect.fail( - new RateLimitExceededError({ - retryAfter, - limit: config.maxRequests, - }) - ) - } - - yield* incrementCount(key) - }), - - getStatus: (key) => - Effect.gen(function* () { - const now = Date.now() - const currentState = yield* getOrCreateState(key, now) - return { - remaining: Math.max(0, config.maxRequests - currentState.count), - resetAt: currentState.resetAt, - } - }), - } - - return limiter - }) - -// ============================================ -// 4. Rate limit middleware -// ============================================ - -const withRateLimit = ( - handler: Effect.Effect -): Effect.Effect< - A | HttpServerResponse.HttpServerResponse, - E, - R | RateLimiterService | HttpServerRequest.HttpServerRequest -> => - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - const rateLimiter = yield* RateLimiterService - - // Use IP address as key (in production, might use user ID or API key) - const clientKey = request.headers["x-forwarded-for"] || "unknown" - - const result = yield* rateLimiter.check(clientKey).pipe( - Effect.matchEffect({ - onFailure: (error) => - Effect.succeed( - HttpServerResponse.json( - { - error: "Rate limit exceeded", - retryAfter: error.retryAfter, - }, - { - status: 429, - headers: { - "Retry-After": String(error.retryAfter), - "X-RateLimit-Limit": String(error.limit), - "X-RateLimit-Remaining": "0", - }, - } - ) - ), - onSuccess: () => handler, - }) - ) - - return result - }) - -// ============================================ -// 5. Usage example -// ============================================ - -const RateLimiterLive = Layer.effect( - RateLimiterService, - makeRateLimiter({ - maxRequests: 100, // 100 requests - windowMs: 60 * 1000, // per minute - }) -) - -const apiEndpoint = withRateLimit( - Effect.gen(function* () { - // Your actual handler logic - return HttpServerResponse.json({ data: "Success!" }) - }) -) -``` - -### Explanation - -Rate limiting protects your API: - -1. **Prevent abuse** - Stop malicious flooding -2. **Fair usage** - Share resources among clients -3. **Cost control** - Limit expensive operations -4. **Stability** - Prevent cascading failures - ---- - ---- - -## Add Timeouts to HTTP Requests - -**Rule:** Always set timeouts on HTTP requests to ensure your application doesn't hang. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Duration, Data } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" - -// ============================================ -// 1. Basic request timeout -// ============================================ - -const fetchWithTimeout = (url: string, timeout: Duration.DurationInput) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.timeout(timeout) - ) - // Returns Option - None if timed out - }) - -// ============================================ -// 2. Timeout with custom error -// ============================================ - -class RequestTimeoutError extends Data.TaggedError("RequestTimeoutError")<{ - readonly url: string - readonly timeout: Duration.Duration -}> {} - -const fetchWithTimeoutError = (url: string, timeout: Duration.DurationInput) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.timeoutFail({ - duration: timeout, - onTimeout: () => new RequestTimeoutError({ - url, - timeout: Duration.decode(timeout), - }), - }) - ) - }) - -// ============================================ -// 3. Different timeouts for different phases -// ============================================ - -const fetchWithPhasedTimeouts = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - // Connection timeout (initial) - const response = yield* client.get(url).pipe( - Effect.timeout("5 seconds"), - Effect.flatten, - Effect.mapError(() => new Error("Connection timeout")) - ) - - // Read timeout (body) - const body = yield* HttpClientResponse.text(response).pipe( - Effect.timeout("30 seconds"), - Effect.flatten, - Effect.mapError(() => new Error("Read timeout")) - ) - - return body - }) - -// ============================================ -// 4. Timeout with fallback -// ============================================ - -interface ApiResponse { - data: unknown - cached: boolean -} - -const fetchWithFallback = (url: string): Effect.Effect => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.map((data) => ({ data, cached: false })), - Effect.timeout("5 seconds"), - Effect.flatMap((result) => - result._tag === "Some" - ? Effect.succeed(result.value) - : Effect.succeed({ data: null, cached: true }) // Fallback - ) - ) - }) - -// ============================================ -// 5. Timeout with interrupt -// ============================================ - -const fetchWithInterrupt = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.interruptible, - Effect.timeout("10 seconds") - ) - // Fiber is interrupted if timeout, freeing resources - }) - -// ============================================ -// 6. Configurable timeout wrapper -// ============================================ - -interface TimeoutConfig { - readonly connect: Duration.DurationInput - readonly read: Duration.DurationInput - readonly total: Duration.DurationInput -} - -const defaultTimeouts: TimeoutConfig = { - connect: "5 seconds", - read: "30 seconds", - total: "60 seconds", -} - -const createHttpClient = (config: TimeoutConfig = defaultTimeouts) => - Effect.gen(function* () { - const baseClient = yield* HttpClient.HttpClient - - return { - get: (url: string) => - baseClient.get(url).pipe( - Effect.timeout(config.connect), - Effect.flatten, - Effect.flatMap((r) => - HttpClientResponse.json(r).pipe( - Effect.timeout(config.read), - Effect.flatten - ) - ), - Effect.timeout(config.total), - Effect.flatten - ), - } - }) - -// ============================================ -// 7. Usage -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("Fetching with timeout...") - - const result = yield* fetchWithTimeoutError( - "https://api.example.com/slow", - "5 seconds" - ).pipe( - Effect.catchTag("RequestTimeoutError", (error) => - Effect.gen(function* () { - yield* Effect.log(`Request to ${error.url} timed out`) - return { error: "timeout" } - }) - ) - ) - - yield* Effect.log(`Result: ${JSON.stringify(result)}`) -}) -``` - -### Explanation - -HTTP requests can hang indefinitely: - -1. **Server issues** - Unresponsive servers -2. **Network problems** - Packets lost -3. **Slow responses** - Large payloads -4. **Resource leaks** - Connections never closed - -Timeouts prevent these from blocking your application. - ---- - ---- - -## Automatically Retry Failed Operations - -**Rule:** Compose a Stream with the .retry(Schedule) operator to automatically recover from transient failures. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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 ==="); - for (let index = 0; index < results.length; index++) { - yield* Effect.log(`Item ${ids[index]}: ${results[index]}`); - } - - yield* Effect.log("✅ Stream processing completed"); -}); - -Effect.runPromise(program).catch((error) => { - Effect.runSync(Effect.logError("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..." -*/ -``` - -### Anti-Pattern - -The anti-pattern is to either have no retry logic at all, or to write manual, imperative retry loops inside your processing function. - -```typescript -import { Effect, Stream } from "effect"; -// ... same mock processItem function ... - -const ids = [1, 2, 3]; - -const program = Stream.fromIterable(ids).pipe( - // No retry logic. The entire stream will fail when item 2 fails. - Stream.mapEffect(processItem, { concurrency: 1 }), - Stream.runDrain -); - -Effect.runPromise(program).catch((error) => { - console.error("Pipeline failed:", 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." -Pipeline failed: Error: API is temporarily down -*/ -``` - -This "fail-fast" approach is brittle. A single, temporary network blip would cause the entire pipeline to terminate, even if subsequent items could have been processed successfully. While manual retry logic inside `processItem` is possible, it pollutes the core logic with concerns about timing and attempt counting, and is far less composable and reusable than a `Schedule`. - -### Explanation - -Real-world systems are unreliable. Network connections drop, APIs return temporary `503` errors, and databases can experience deadlocks. A naive pipeline will fail completely on the first sign of trouble. A resilient pipeline, however, can absorb these transient errors and heal itself. - -The `retry` operator, combined with the `Schedule` module, provides a powerful and declarative way to build this resilience: - -1. **Declarative Resilience**: Instead of writing complex `try/catch` loops with manual delay logic, you declaratively state _how_ the pipeline should retry. For example, "retry 3 times, with an exponential backoff starting at 100ms." -2. **Separation of Concerns**: Your core pipeline logic remains focused on the "happy path." The retry strategy is a separate, composable concern that you apply to the entire stream. -3. **Rich Scheduling Policies**: `Schedule` is incredibly powerful. You can create schedules based on a fixed number of retries, exponential backoff, jitter (to avoid thundering herd problems), or even combinations of these. -4. **Prevents Cascading Failures**: By handling temporary issues at the source, you prevent a small, transient glitch from causing a complete failure of your entire application. - ---- - ---- - -## Avoid Long Chains of .andThen; Use Generators Instead - -**Rule:** Prefer generators over long chains of .andThen. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const result = yield* program; - yield* Effect.log(`Final result: ${result}`); - return result; -}); - -Effect.runPromise(programWithLogging); -``` - -**Explanation:** -Generators keep sequential logic readable and easy to maintain. - -### Anti-Pattern - -```typescript -import { Effect } from "effect"; -declare const step1: () => Effect.Effect; -declare const step2: (a: any) => Effect.Effect; - -step1().pipe(Effect.flatMap((a) => step2(a))); // Or .andThen -``` - -Chaining many `.flatMap` or `.andThen` calls leads to deeply nested, -hard-to-read code. - -### Explanation - -`Effect.gen` provides a flat, linear code structure that is easier to read and -debug than deeply nested functional chains. - ---- - -## 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. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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"); - yield* Effect.log("Login event:", loginEvent); - - const logoutEvent = yield* createEvent("User logged out"); - yield* Effect.log("Logout event:", logoutEvent); -}); - -// Run the program -const programWithErrorHandling = program.pipe( - Effect.provideService(Clock.Clock, Clock.make()), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Program error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithErrorHandling); -``` - ---- - -### Anti-Pattern - -Directly using `Date.now()` or `new Date()` inside your effects. This introduces impurity and makes your logic dependent on the actual system clock, rendering it non-deterministic and difficult to test. - -```typescript -import { Effect } from "effect"; - -// ❌ WRONG: This function is impure and not reliably testable. -const createEventUnsafely = (message: string): Effect.Effect => - Effect.sync(() => ({ - message, - timestamp: Date.now(), // Direct call to a system API - })); - -// How would you test that this function assigns the correct timestamp -// without manipulating the system clock or using complex mocks? -``` - -### Explanation - -JavaScript's native `Date` object is a common source of bugs. It is mutable, its behavior can be inconsistent across different JavaScript environments (especially with timezones), and its reliance on the system clock makes time-dependent logic difficult to test. - -Effect's approach solves these problems: - -- The **`Clock` service** abstracts away the concept of "now." In production, the `Live` clock uses the system time. In tests, you can provide a `TestClock` that gives you complete, deterministic control over the passage of time. -- Using **primitive `number` or `string`** for timestamps ensures immutability and makes your data easy to serialize, store, and transfer. - -This makes your time-based logic pure, predictable, and easy to test. - ---- - ---- - -## Build a Basic HTTP Server - -**Rule:** Use a managed Runtime created from a Layer to handle requests in a Node.js HTTP server. - -**Skill Level:** advanced - -**Use Cases:** making-http-requests - -### Good Example - -This example creates a simple server with a `Greeter` service. The server starts, creates a runtime containing the `Greeter`, and then uses that runtime to handle requests. - -```typescript -import { HttpServer, HttpServerResponse } from "@effect/platform"; -import { NodeHttpServer } from "@effect/platform-node"; -import { Duration, Effect, Fiber, Layer } from "effect"; -import { createServer } from "node:http"; - -// Create a server layer using Node's built-in HTTP server -const ServerLive = NodeHttpServer.layer(() => createServer(), { port: 3001 }); - -// Define your HTTP app (here responding "Hello World" to every request) -const app = Effect.gen(function* () { - yield* Effect.logInfo("Received HTTP request"); - return yield* HttpServerResponse.text("Hello World"); -}); - -const serverLayer = HttpServer.serve(app).pipe(Layer.provide(ServerLive)); - -const program = Effect.gen(function* () { - yield* Effect.logInfo("Server starting on http://localhost:3001"); - const fiber = yield* Layer.launch(serverLayer).pipe(Effect.fork); - yield* Effect.sleep(Duration.seconds(2)); - yield* Fiber.interrupt(fiber); - yield* Effect.logInfo("Server shutdown complete"); -}); - -Effect.runPromise(program as unknown as Effect.Effect); -``` - ---- - -### Anti-Pattern - -Creating a new runtime or rebuilding layers for every single incoming request. This is extremely inefficient and defeats the purpose of Effect's `Layer` system. - -```typescript -import * as http from "http"; -import { Effect, Layer } from "effect"; -import { GreeterLive } from "./somewhere"; - -// ❌ WRONG: This rebuilds the GreeterLive layer on every request. -const server = http.createServer((_req, res) => { - const requestEffect = Effect.succeed("Hello!").pipe( - Effect.provide(GreeterLive) // Providing the layer here is inefficient - ); - Effect.runPromise(requestEffect).then((msg) => res.end(msg)); -}); -``` - -### Explanation - -This pattern demonstrates the complete lifecycle of a long-running Effect application. - -1. **Setup Phase:** You define all your application's dependencies (database connections, clients, config) in `Layer`s and compose them into a single `AppLayer`. -2. **Runtime Creation:** You use `Layer.toRuntime(AppLayer)` to create a highly-optimized `Runtime` object. This is done _once_ when the server starts. -3. **Request Handling:** For each incoming request, you create an `Effect` that describes the work to be done (e.g., parse request, call services, create response). -4. **Execution:** You use the `Runtime` you created in the setup phase to execute the request-handling `Effect` using `Runtime.runPromise`. - -This architecture ensures that your request handling logic is fully testable, benefits from structured concurrency, and is completely decoupled from the server's setup and infrastructure. - ---- - ---- - -## Cache HTTP Responses - -**Rule:** Use an in-memory or persistent cache to store HTTP responses. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Ref, HashMap, Option, Duration } from "effect" -import { HttpClient, HttpClientResponse } from "@effect/platform" - -// ============================================ -// 1. Simple in-memory cache -// ============================================ - -interface CacheEntry { - readonly data: T - readonly timestamp: number - readonly ttl: number -} - -const makeCache = () => - Effect.gen(function* () { - const store = yield* Ref.make(HashMap.empty>()) - - const get = (key: string): Effect.Effect> => - Ref.get(store).pipe( - Effect.map((map) => { - const entry = HashMap.get(map, key) - if (entry._tag === "None") return Option.none() - - const now = Date.now() - if (now > entry.value.timestamp + entry.value.ttl) { - return Option.none() // Expired - } - return Option.some(entry.value.data) - }) - ) - - const set = (key: string, data: T, ttl: number): Effect.Effect => - Ref.update(store, (map) => - HashMap.set(map, key, { - data, - timestamp: Date.now(), - ttl, - }) - ) - - const invalidate = (key: string): Effect.Effect => - Ref.update(store, (map) => HashMap.remove(map, key)) - - const clear = (): Effect.Effect => - Ref.set(store, HashMap.empty()) - - return { get, set, invalidate, clear } - }) - -// ============================================ -// 2. Cached HTTP client -// ============================================ - -interface CachedHttpClient { - readonly get: ( - url: string, - options?: { ttl?: Duration.DurationInput } - ) => Effect.Effect - readonly invalidate: (url: string) => Effect.Effect -} - -const makeCachedHttpClient = Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - const cache = yield* makeCache() - - const client: CachedHttpClient = { - get: (url: string, options?: { ttl?: Duration.DurationInput }) => { - const ttl = options?.ttl ? Duration.toMillis(Duration.decode(options.ttl)) : 60000 - - return Effect.gen(function* () { - // Check cache first - const cached = yield* cache.get(url) - if (Option.isSome(cached)) { - yield* Effect.log(`Cache hit: ${url}`) - return cached.value as T - } - - yield* Effect.log(`Cache miss: ${url}`) - - // Fetch from network - const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect - - // Store in cache - yield* cache.set(url, data, ttl) - - return data - }) - }, - - invalidate: (url) => cache.invalidate(url), - } - - return client -}) - -// ============================================ -// 3. Stale-while-revalidate pattern -// ============================================ - -interface SWRCache { - readonly data: T - readonly timestamp: number - readonly staleAfter: number - readonly expireAfter: number -} - -const makeSWRClient = Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - const cache = yield* Ref.make(HashMap.empty>()) - - return { - get: ( - url: string, - options: { - staleAfter: Duration.DurationInput - expireAfter: Duration.DurationInput - } - ) => - Effect.gen(function* () { - const now = Date.now() - const staleMs = Duration.toMillis(Duration.decode(options.staleAfter)) - const expireMs = Duration.toMillis(Duration.decode(options.expireAfter)) - - const cached = yield* Ref.get(cache).pipe( - Effect.map((map) => HashMap.get(map, url)) - ) - - if (cached._tag === "Some") { - const entry = cached.value - const age = now - entry.timestamp - - if (age < staleMs) { - // Fresh - return immediately - return entry.data as T - } - - if (age < expireMs) { - // Stale - return cached, revalidate in background - yield* Effect.fork( - httpClient.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.flatMap((data) => - Ref.update(cache, (map) => - HashMap.set(map, url, { - data, - timestamp: Date.now(), - staleAfter: staleMs, - expireAfter: expireMs, - }) - ) - ), - Effect.catchAll(() => Effect.void) // Ignore errors - ) - ) - return entry.data as T - } - } - - // Expired or missing - fetch fresh - const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect - - yield* Ref.update(cache, (map) => - HashMap.set(map, url, { - data, - timestamp: now, - staleAfter: staleMs, - expireAfter: expireMs, - }) - ) - - return data - }), - } -}) - -// ============================================ -// 4. Cache with request deduplication -// ============================================ - -const makeDeduplicatedClient = Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - const inFlight = yield* Ref.make(HashMap.empty>()) - const cache = yield* makeCache() - - return { - get: (url: string, ttl: number = 60000) => - Effect.gen(function* () { - // Check cache - const cached = yield* cache.get(url) - if (Option.isSome(cached)) { - return cached.value as T - } - - // Check if request already in flight - const pending = yield* Ref.get(inFlight).pipe( - Effect.map((map) => HashMap.get(map, url)) - ) - - if (pending._tag === "Some") { - yield* Effect.log(`Deduplicating request: ${url}`) - return (yield* pending.value) as T - } - - // Make the request - const request = httpClient.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.tap((data) => cache.set(url, data, ttl)), - Effect.ensuring( - Ref.update(inFlight, (map) => HashMap.remove(map, url)) - ) - ) - - // Store in-flight request - yield* Ref.update(inFlight, (map) => HashMap.set(map, url, request)) - - return (yield* request) as T - }), - } -}) - -// ============================================ -// 5. Usage -// ============================================ - -const program = Effect.gen(function* () { - const client = yield* makeCachedHttpClient - - // First call - cache miss - yield* client.get("https://api.example.com/users/1", { ttl: "5 minutes" }) - - // Second call - cache hit - yield* client.get("https://api.example.com/users/1") - - // Invalidate when data changes - yield* client.invalidate("https://api.example.com/users/1") -}) -``` - -### Explanation - -Caching provides: - -1. **Performance** - Avoid redundant network calls -2. **Cost reduction** - Fewer API calls -3. **Resilience** - Serve stale data when API is down -4. **Rate limit safety** - Stay under quotas - ---- - ---- - -## Chaining Computations with flatMap - -**Rule:** Use flatMap to sequence computations, flattening nested structures and preserving error and context handling. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Stream, Option, Either } from "effect"; - -// Effect: Chain two effectful computations -const effect = Effect.succeed(2).pipe( - Effect.flatMap((n) => Effect.succeed(n * 10)) -); // Effect - -// Option: Chain two optional computations -const option = Option.some(2).pipe(Option.flatMap((n) => Option.some(n * 10))); // Option - -// Either: Chain two computations that may fail -const either = Either.right(2).pipe( - Either.flatMap((n) => Either.right(n * 10)) -); // Either - -// Stream: Chain streams (flattening) -const stream = Stream.fromIterable([1, 2]).pipe( - Stream.flatMap((n) => Stream.fromIterable([n, n * 10])) -); // Stream -``` - -**Explanation:** -`flatMap` lets you build pipelines where each step can depend on the result of the previous one, and the structure is always flattened—no `Option>` or `Effect>`. - -### Anti-Pattern - -Manually unwrapping the value (e.g., with `.getOrElse`, `.unsafeRunSync`, etc.), then creating a new effect/option/either/stream. -This breaks composability, loses error/context handling, and leads to deeply nested or unsafe code. - -### Explanation - -`flatMap` is the key to sequencing dependent steps in functional programming. -It allows you to express workflows where each step may fail, be optional, or produce multiple results, and ensures that errors and context are handled automatically. - ---- - -## Checking Option and Either Cases - -**Rule:** Use isSome, isNone, isLeft, and isRight to check Option and Either cases for simple, type-safe conditional logic. - -**Skill Level:** beginner - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Option, Either } from "effect"; - -// Option: Check if value is Some or None -const option = Option.some(42); - -if (Option.isSome(option)) { - // option.value is available here - console.log("We have a value:", option.value); -} else if (Option.isNone(option)) { - console.log("No value present"); -} - -// Either: Check if value is Right or Left -const either = Either.left("error"); - -if (Either.isRight(either)) { - // either.right is available here - console.log("Success:", either.right); -} else if (Either.isLeft(either)) { - // either.left is available here - console.log("Failure:", either.left); -} - -// Filtering a collection of Options -const options = [Option.some(1), Option.none(), Option.some(3)]; -const presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3] -``` - -**Explanation:** - -- `Option.isSome` and `Option.isNone` let you check for presence or absence. -- `Either.isRight` and `Either.isLeft` let you check for success or failure. -- These are especially useful for filtering or quick conditional logic. - -### Anti-Pattern - -Manually checking internal tags or properties (e.g., `option._tag === "Some"`), or using unsafe type assertions, which is less safe and less readable than using the provided predicates. - -### Explanation - -These predicates provide a concise, type-safe way to check which case you have, without resorting to manual property checks or unsafe type assertions. - ---- - -## Collect All Results into a List - -**Rule:** Use Stream.runCollect to execute a stream and collect all its emitted values into a Chunk. - -**Skill Level:** beginner - -**Use Cases:** building-data-pipelines - -### Good Example - -This example creates a stream of numbers, filters for only the even ones, transforms them into strings, and then uses `runCollect` to gather the final results into a `Chunk`. - -```typescript -import { Effect, Stream, Chunk } from "effect"; - -const program = Stream.range(1, 10).pipe( - // Find all the even numbers - Stream.filter((n) => n % 2 === 0), - // Transform them into strings - Stream.map((n) => `Even number: ${n}`), - // Run the stream and collect the results - Stream.runCollect -); - -const programWithLogging = Effect.gen(function* () { - const results = yield* program; - yield* Effect.log( - `Collected results: ${JSON.stringify(Chunk.toArray(results))}` - ); - return results; -}); - -Effect.runPromise(programWithLogging); -/* -Output: -Collected results: [ - 'Even number: 2', - 'Even number: 4', - 'Even number: 6', - 'Even number: 8', - 'Even number: 10' -] -*/ -``` - -### Anti-Pattern - -The anti-pattern is using `Stream.runCollect` on a stream that produces an unbounded or extremely large number of items. This will inevitably lead to an out-of-memory error. - -```typescript -import { Effect, Stream } from "effect"; - -// An infinite stream of numbers -const infiniteStream = Stream.range(1, Infinity); - -const program = infiniteStream.pipe( - // This will run forever, attempting to buffer an infinite number of items. - Stream.runCollect -); - -// This program will never finish and will eventually crash the process -// by consuming all available memory. -// Effect.runPromise(program); -console.log( - "This code is commented out because it would cause an out-of-memory crash." -); -``` - -This is a critical mistake because `runCollect` must hold every single item emitted by the stream in memory simultaneously. For pipelines that process huge files, infinite data sources, or are designed to run forever, `runCollect` is the wrong tool. In those cases, you should use a sink like `Stream.runDrain`, which processes items without collecting them. - -### Explanation - -A "sink" is a terminal operator that consumes a stream and produces a final `Effect`. `Stream.runCollect` is the most fundamental sink. It provides the bridge from the lazy, pull-based world of `Stream` back to the familiar world of a single `Effect` that resolves with a standard data structure. - -Using `Stream.runCollect` is essential when: - -1. **You Need the Final Result**: The goal of your pipeline is to produce a complete list of transformed items that you need to use in a subsequent step (e.g., to return as a single JSON array from an API). -2. **Simplicity is Key**: It's the most straightforward way to "run" a stream and see its output. It declaratively states your intent: "execute this entire pipeline and give me all the results." -3. **The Dataset is Bounded**: It's designed for streams where the total number of items is known to be finite and small enough to fit comfortably in memory. - -The result of `Stream.runCollect` is an `Effect` that, when executed, yields a `Chunk` containing all the items emitted by the stream. - ---- - ---- - -## Combining Values with zip - -**Rule:** Use zip to run two computations and combine their results into a tuple, preserving error and context handling. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Either, Option, Stream } from "effect"; - -// Effect: Combine two effects and get both results -const effectA = Effect.succeed(1); -const effectB = Effect.succeed("hello"); -const zippedEffect = effectA.pipe(Effect.zip(effectB)); // Effect<[number, string]> - -// Option: Combine two options, only Some if both are Some -const optionA = Option.some(1); -const optionB = Option.some("hello"); -const zippedOption = Option.all([optionA, optionB]); // Option<[number, string]> - -// Either: Combine two eithers, only Right if both are Right -const eitherA = Either.right(1); -const eitherB = Either.right("hello"); -const zippedEither = Either.all([eitherA, eitherB]); // Either - -// Stream: Pair up values from two streams -const streamA = Stream.fromIterable([1, 2, 3]); -const streamB = Stream.fromIterable(["a", "b", "c"]); -const zippedStream = streamA.pipe(Stream.zip(streamB)); // Stream<[number, string]> -``` - -**Explanation:** -`zip` runs both computations and pairs their results. -If either computation fails (or is None/Left/empty), the result is a failure (or None/Left/empty). - -### Anti-Pattern - -Manually running two computations, extracting their results, and pairing them outside the combinator world. -This breaks composability, loses error/context handling, and can lead to subtle bugs. - -### Explanation - -`zip` lets you compose computations that are independent but whose results you want to use together. -It preserves error handling and context, and keeps your code declarative and type-safe. - ---- - -## Comparing Data by Value with Data.struct - -**Rule:** Use Data.struct to define objects whose equality is based on their contents, enabling safe and predictable comparisons. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Data, Equal } from "effect"; - -// Create two structurally equal objects -const user1 = Data.struct({ id: 1, name: "Alice" }); -const user2 = Data.struct({ id: 1, name: "Alice" }); - -// Compare by value, not reference -const areEqual = Equal.equals(user1, user2); // true - -// Use in a HashSet or as keys in a Map -import { HashSet } from "effect"; -const set = HashSet.make(user1); -console.log(HashSet.has(set, user2)); // true -``` - -**Explanation:** - -- `Data.struct` creates immutable objects with value-based equality. -- Use for domain entities, value objects, and when storing objects in sets or as map keys. -- Avoids bugs from reference-based comparison. - -### Anti-Pattern - -Using plain JavaScript objects for value-based logic, which compares by reference and can lead to incorrect equality checks and collection behavior. - -### Explanation - -JavaScript objects are compared by reference, which can lead to subtle bugs when modeling value objects. -`Data.struct` ensures that two objects with the same contents are considered equal, supporting value-based logic and collections. - ---- - -## Comparing Data by Value with Structural Equality - -**Rule:** Use Data.struct or implement the Equal interface for value-based comparison of objects and classes. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -We define two points using `Data.struct`. Even though `p1` and `p2` are different instances in memory, `Equal.equals` correctly reports them as equal because their contents match. - -```typescript -import { Data, Equal, Effect } from "effect"; - -// Define a Point type with structural equality -interface Point { - readonly _tag: "Point"; - readonly x: number; - readonly y: number; -} - -const Point = Data.tagged("Point"); - -// Create a program to demonstrate structural equality -const program = Effect.gen(function* () { - const p1 = Point({ x: 1, y: 2 }); - const p2 = Point({ x: 1, y: 2 }); - const p3 = Point({ x: 3, y: 4 }); - - // Standard reference equality fails - yield* Effect.log("Comparing points with reference equality (===):"); - yield* Effect.log(`p1 === p2: ${p1 === p2}`); - - // Structural equality works as expected - yield* Effect.log("\nComparing points with structural equality:"); - yield* Effect.log(`p1 equals p2: ${Equal.equals(p1, p2)}`); - yield* Effect.log(`p1 equals p3: ${Equal.equals(p1, p3)}`); - - // Show the actual points - yield* Effect.log("\nPoint values:"); - yield* Effect.log(`p1: ${JSON.stringify(p1)}`); - yield* Effect.log(`p2: ${JSON.stringify(p2)}`); - yield* Effect.log(`p3: ${JSON.stringify(p3)}`); -}); - -// Run the program -Effect.runPromise(program); -``` - ---- - -### Anti-Pattern - -Relying on `===` for object or array comparison. This will lead to bugs when you expect two objects with the same values to be treated as equal, especially when working with data in collections, `Ref`s, or `Effect`'s success values. - -```typescript -// ❌ WRONG: This will not behave as expected. -const user1 = { id: 1, name: "Paul" }; -const user2 = { id: 1, name: "Paul" }; - -if (user1 === user2) { - // This code block will never be reached. - console.log("Users are the same."); -} - -// Another common pitfall -const selectedUsers = [user1]; -// This check will fail, even though a user with id 1 is in the array. -if (selectedUsers.includes({ id: 1, name: "Paul" })) { - // ... -} -``` - -### Explanation - -In JavaScript, comparing two non-primitive values with `===` checks for _referential equality_. It only returns `true` if they are the exact same instance in memory. This means two objects with identical contents are not considered equal, which is a common source of bugs. - -```typescript -{ a: 1 } === { a: 1 } // false! -``` - -Effect solves this with **structural equality**. All of Effect's built-in data structures (`Option`, `Either`, `Chunk`, etc.) can be compared by their structure and values. By using helpers like `Data.struct`, you can easily give your own data structures this same powerful and predictable behavior. - ---- - ---- - -## Compose API Middleware - -**Rule:** Use Effect composition to build a middleware pipeline that processes requests. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good Example - -```typescript -import { Effect, Context, Layer, Duration } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. Define middleware type -// ============================================ - -type Handler = Effect.Effect - -type Middleware = ( - handler: Handler -) => Handler - -// ============================================ -// 2. Logging middleware -// ============================================ - -const withLogging: Middleware = - (handler) => - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - const startTime = Date.now() - - yield* Effect.log(`→ ${request.method} ${request.url}`) - - const response = yield* handler - - const duration = Date.now() - startTime - yield* Effect.log(`← ${response.status} (${duration}ms)`) - - return response - }) - -// ============================================ -// 3. Timing middleware (adds header) -// ============================================ - -const withTiming: Middleware = (handler) => - Effect.gen(function* () { - const startTime = Date.now() - const response = yield* handler - const duration = Date.now() - startTime - - return HttpServerResponse.setHeader( - response, - "X-Response-Time", - `${duration}ms` - ) - }) - -// ============================================ -// 4. Error handling middleware -// ============================================ - -const withErrorHandling: Middleware = (handler) => - handler.pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Unhandled error: ${error}`) - - return HttpServerResponse.json( - { error: "Internal Server Error" }, - { status: 500 } - ) - }) - ) - ) - -// ============================================ -// 5. Request ID middleware -// ============================================ - -class RequestId extends Context.Tag("RequestId")() {} - -const withRequestId: Middleware = (handler) => - Effect.gen(function* () { - const requestId = crypto.randomUUID() - - const response = yield* handler.pipe( - Effect.provideService(RequestId, requestId) - ) - - return HttpServerResponse.setHeader(response, "X-Request-Id", requestId) - }) - -// ============================================ -// 6. Timeout middleware -// ============================================ - -const withTimeout = (duration: Duration.DurationInput): Middleware => - (handler) => - handler.pipe( - Effect.timeout(duration), - Effect.catchTag("TimeoutException", () => - Effect.succeed( - HttpServerResponse.json( - { error: "Request timeout" }, - { status: 504 } - ) - ) - ) - ) - -// ============================================ -// 7. CORS middleware (see separate pattern) -// ============================================ - -const withCORS = (origin: string): Middleware => (handler) => - Effect.gen(function* () { - const response = yield* handler - - return response.pipe( - HttpServerResponse.setHeader("Access-Control-Allow-Origin", origin), - HttpServerResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"), - HttpServerResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization") - ) - }) - -// ============================================ -// 8. Compose middleware -// ============================================ - -const applyMiddleware = (handler: Handler) => - handler.pipe( - withLogging, - withTiming, - withRequestId, - withTimeout("30 seconds"), - withCORS("*"), - withErrorHandling - ) - -// ============================================ -// 9. Usage -// ============================================ - -const myHandler = Effect.gen(function* () { - const requestId = yield* RequestId - yield* Effect.log(`Processing request ${requestId}`) - - return HttpServerResponse.json({ message: "Hello!" }) -}) - -const protectedHandler = applyMiddleware(myHandler) -``` - -### Explanation - -Middleware provides separation of concerns: - -1. **Reusability** - Write once, apply everywhere -2. **Composability** - Stack multiple middlewares -3. **Testability** - Test each middleware in isolation -4. **Clarity** - Handlers focus on business logic - ---- - ---- - -## Compose Resource Lifecycles with `Layer.merge` - -**Rule:** Compose multiple scoped layers using `Layer.merge` or by providing one layer to another. - -**Skill Level:** intermediate - -**Use Cases:** resource-management - -### Good 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* Effect.log(dbResult); - yield* Effect.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. - -### Anti-Pattern - -A manual, imperative startup and shutdown script. This approach is brittle and error-prone. The developer is responsible for maintaining the correct order of initialization and, more importantly, the reverse order for shutdown. This becomes unmanageable as an application grows. - -```typescript -// ANTI-PATTERN: Manual, brittle, and error-prone -async function main() { - const db = await initDb(); // acquire 1 - const client = await initApiClient(); // acquire 2 - - try { - await doWork(db, client); // use - } finally { - // This order is easy to get wrong! - await client.close(); // release 2 - await db.close(); // release 1 - } -} -``` - -### Explanation - -This pattern is the ultimate payoff for defining services with `Layer`. It allows for true modularity. Each service can be defined in its own file, declaring its own resource requirements in its `Live` layer, completely unaware of other services. - -When you assemble the final application layer, Effect analyzes the dependencies: - -1. **Acquisition Order:** It ensures resources are acquired in the correct order. For example, a `Logger` layer might be initialized before a `Database` layer that uses it for logging. -2. **Release Order:** It guarantees that resources are released in the **exact reverse order** of their acquisition. This is critical for preventing shutdown errors, such as a `UserRepository` trying to log a final message after the `Logger` has already been shut down. - -This automates one of the most complex and error-prone parts of application architecture. - ---- - -## Concurrency Pattern 1: Coordinate Async Operations with Deferred - -**Rule:** Use Deferred for one-time async coordination between fibers, enabling multiple consumers to wait for a single producer's result. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates a service startup pattern where multiple workers wait for initialization to complete before starting processing. - -```typescript -import { Effect, Deferred, Fiber } from "effect"; - -interface ServiceConfig { - readonly name: string; - readonly port: number; -} - -interface Service { - readonly name: string; - readonly isReady: Deferred.Deferred; -} - -// Simulate a service that takes time to initialize -const createService = (config: ServiceConfig): Effect.Effect => - Effect.gen(function* () { - const isReady = yield* Deferred.make(); - - return { name: config.name, isReady }; - }); - -// Initialize the service (runs in background) -const initializeService = (service: Service): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[${service.name}] Starting initialization...`); - - // Simulate initialization work - yield* Effect.sleep("1 second"); - - yield* Effect.log(`[${service.name}] Initialization complete`); - - // Signal that service is ready - yield* Deferred.succeed(service.isReady, undefined); - }); - -// A worker that waits for service to be ready before starting -const createWorker = ( - id: number, - services: Service[] -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[Worker ${id}] Starting, waiting for services...`); - - // Wait for all services to be ready - yield* Effect.all( - services.map((service) => - Deferred.await(service.isReady).pipe( - Effect.tapError((error) => - Effect.log( - `[Worker ${id}] Error waiting for ${service.name}: ${error}` - ) - ) - ) - ) - ); - - yield* Effect.log(`[Worker ${id}] All services ready, starting work`); - - // Simulate worker processing - for (let i = 0; i < 3; i++) { - yield* Effect.sleep("500 millis"); - yield* Effect.log(`[Worker ${id}] Processing task ${i + 1}`); - } - - yield* Effect.log(`[Worker ${id}] Complete`); - }); - -// Main program -const program = Effect.gen(function* () { - // Create services - const apiService = yield* createService({ name: "API", port: 3000 }); - const dbService = yield* createService({ name: "Database", port: 5432 }); - const cacheService = yield* createService({ name: "Cache", port: 6379 }); - - const services = [apiService, dbService, cacheService]; - - // Start initializing services in background - const initFibers = yield* Effect.all( - services.map((service) => initializeService(service).pipe(Effect.fork)) - ); - - // Start workers that wait for services - const workerFibers = yield* Effect.all( - [1, 2, 3].map((id) => createWorker(id, services).pipe(Effect.fork)) - ); - - // Wait for all workers to complete - yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber))); - - // Cancel initialization fibers (they're done anyway) - yield* Effect.all(initFibers.map((fiber) => Fiber.interrupt(fiber))); - - yield* Effect.log(`\n[MAIN] All workers completed`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Creates Deferred instances** for each service's readiness -2. **Starts initialization** in background fibers -3. **Workers wait** for all services via `Deferred.await` -4. **Service signals completion** via `Deferred.succeed` -5. **Workers resume** when all dependencies ready - ---- - -### Explanation - -Many concurrent systems need to coordinate on events: - -- **Service initialization**: Wait for all services to start before accepting requests -- **Data availability**: Wait for initial data load before processing -- **External events**: Wait for webhook, signal, or message -- **Startup gates**: All workers wait for leader to signal start - -Without Deferred: - -- Polling wastes CPU (check repeatedly) -- Callbacks become complex (multiple consumers) -- No clean semantics for "wait for this one thing" -- Error propagation unclear - -With Deferred: - -- Non-blocking wait (fiber suspends) -- One fiber produces, many consume -- Clear completion or failure -- Efficient wakeup when ready - ---- - ---- - -## Concurrency Pattern 2: Rate Limit Concurrent Access with Semaphore - -**Rule:** Use Semaphore to limit concurrent access to resources, preventing overload and enabling fair resource distribution. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates limiting concurrent database connections using a Semaphore, preventing connection pool exhaustion. - -```typescript -import { Effect, Semaphore, Fiber } from "effect"; - -interface QueryResult { - readonly id: number; - readonly result: string; - readonly duration: number; -} - -// Simulate a database query that holds a connection -const executeQuery = ( - queryId: number, - connectionId: number, - durationMs: number -): Effect.Effect => - Effect.gen(function* () { - const startTime = Date.now(); - - yield* Effect.log( - `[Query ${queryId}] Using connection ${connectionId}, duration: ${durationMs}ms` - ); - - // Simulate query execution - yield* Effect.sleep(`${durationMs} millis`); - - const duration = Date.now() - startTime; - - return { - id: queryId, - result: `Result from query ${queryId}`, - duration, - }; - }); - -// Pool configuration -interface ConnectionPoolConfig { - readonly maxConnections: number; - readonly queryTimeout?: number; -} - -// Create a rate-limited query executor -const createRateLimitedQueryExecutor = ( - config: ConnectionPoolConfig -): Effect.Effect< - (queryId: number, durationMs: number) => Effect.Effect -> => - Effect.gen(function* () { - const semaphore = yield* Semaphore.make(config.maxConnections); - let connectionCounter = 0; - - return (queryId: number, durationMs: number) => - Effect.gen(function* () { - // Acquire a permit (wait if none available) - yield* Semaphore.acquire(semaphore); - - const connectionId = ++connectionCounter; - - // Use try-finally to ensure permit is released - const result = yield* executeQuery(queryId, connectionId, durationMs).pipe( - Effect.ensuring( - Semaphore.release(semaphore).pipe( - Effect.tap(() => - Effect.log(`[Query ${queryId}] Released connection ${connectionId}`) - ) - ) - ) - ); - - return result; - }); - }); - -// Simulate multiple queries arriving -const program = Effect.gen(function* () { - const executor = yield* createRateLimitedQueryExecutor({ - maxConnections: 3, // Only 3 concurrent connections - }); - - // Generate 10 queries with varying durations - const queries = Array.from({ length: 10 }, (_, i) => ({ - id: i + 1, - duration: 500 + Math.random() * 1500, // 500-2000ms - })); - - console.log(`\n[POOL] Starting with max 3 concurrent connections\n`); - - // Execute all queries with concurrency limit - const results = yield* Effect.all( - queries.map((q) => - executor(q.id, Math.round(q.duration)).pipe(Effect.fork) - ) - ).pipe( - Effect.andThen((fibers) => - Effect.all(fibers.map((fiber) => Fiber.join(fiber))) - ) - ); - - console.log(`\n[POOL] All queries completed\n`); - - // Summary - const totalDuration = results.reduce((sum, r) => sum + r.duration, 0); - const avgDuration = totalDuration / results.length; - - console.log(`[SUMMARY]`); - console.log(` Total queries: ${results.length}`); - console.log(` Avg duration: ${Math.round(avgDuration)}ms`); - console.log(` Total time: ${Math.max(...results.map((r) => r.duration))}ms (parallel)`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Creates a Semaphore** with fixed permit count -2. **Acquires permit** before using connection -3. **Executes operation** while holding permit -4. **Releases permit** in finally block (guaranteed) -5. **Fair queuing** of waiting queries - ---- - -### Explanation - -Resource constraints require limiting concurrency: - -- **Connection pools**: Database limited to N connections -- **API rate limits**: Service allows only M requests per second -- **Memory limits**: Large operations can't all run simultaneously -- **CPU constraints**: Too many threads waste cycles on context switching -- **Backpressure**: Prevent downstream from being overwhelmed - -Without Semaphore: - -- All operations run simultaneously, exhausting resources -- Connection pool overflows, requests fail -- Memory pressure causes garbage collection pauses -- No fair ordering (first-come-first-served) - -With Semaphore: - -- Fixed concurrency limit -- Fair queuing of waiting operations -- Backpressure naturally flows upstream -- Clear ownership of permits - ---- - ---- - -## Concurrency Pattern 3: Coordinate Multiple Fibers with Latch - -**Rule:** Use Latch to coordinate multiple fibers awaiting a common completion signal, enabling fan-out/fan-in and barrier synchronization patterns. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates a fan-out/fan-in pattern: spawn 5 worker fibers that process tasks in parallel, and coordinate to know when all are complete. - -```typescript -import { Effect, Latch, Fiber, Ref } from "effect"; - -interface WorkResult { - readonly workerId: number; - readonly taskId: number; - readonly result: string; - readonly duration: number; -} - -// Simulate a long-running task -const processTask = ( - workerId: number, - taskId: number -): Effect.Effect => - Effect.gen(function* () { - const startTime = Date.now(); - const duration = 100 + Math.random() * 400; // 100-500ms - - yield* Effect.log( - `[Worker ${workerId}] Starting task ${taskId} (duration: ${Math.round(duration)}ms)` - ); - - yield* Effect.sleep(`${Math.round(duration)} millis`); - - const elapsed = Date.now() - startTime; - - yield* Effect.log( - `[Worker ${workerId}] ✓ Completed task ${taskId} in ${elapsed}ms` - ); - - return { - workerId, - taskId, - result: `Result from worker ${workerId} on task ${taskId}`, - duration: elapsed, - }; - }); - -// Fan-out/Fan-in with Latch -const fanOutFanIn = Effect.gen(function* () { - const numWorkers = 5; - const tasksPerWorker = 3; - - // Create latch: will countdown from (numWorkers) when all workers complete - const workersCompleteLatch = yield* Latch.make(numWorkers); - - // Track results from all workers - const results = yield* Ref.make([]); - - // Worker fiber that processes tasks sequentially - const createWorker = (workerId: number) => - Effect.gen(function* () { - try { - yield* Effect.log(`[Worker ${workerId}] ▶ Starting`); - - // Process multiple tasks - for (let i = 1; i <= tasksPerWorker; i++) { - const result = yield* processTask(workerId, i); - yield* Ref.update(results, (rs) => [...rs, result]); - } - - yield* Effect.log(`[Worker ${workerId}] ✓ All tasks completed`); - } finally { - // Signal completion to latch - yield* Latch.countDown(workersCompleteLatch); - yield* Effect.log(`[Worker ${workerId}] Signaled latch`); - } - }); - - // Spawn all workers as background fibers - console.log(`\n[COORDINATOR] Spawning ${numWorkers} workers...\n`); - - const workerFibers = yield* Effect.all( - Array.from({ length: numWorkers }, (_, i) => - createWorker(i + 1).pipe(Effect.fork) - ) - ); - - // Wait for all workers to complete - console.log(`\n[COORDINATOR] Waiting for all workers to finish...\n`); - - yield* Latch.await(workersCompleteLatch); - - console.log(`\n[COORDINATOR] All workers completed!\n`); - - // Join all fibers to ensure cleanup - yield* Effect.all(workerFibers.map((fiber) => Fiber.join(fiber))); - - // Aggregate results - const allResults = yield* Ref.get(results); - - console.log(`[SUMMARY]`); - console.log(` Total workers: ${numWorkers}`); - console.log(` Tasks per worker: ${tasksPerWorker}`); - console.log(` Total tasks: ${allResults.length}`); - console.log( - ` Avg task duration: ${Math.round( - allResults.reduce((sum, r) => sum + r.duration, 0) / allResults.length - )}ms` - ); -}); - -Effect.runPromise(fanOutFanIn); -``` - -This pattern: - -1. **Creates Latch** with count = number of workers -2. **Spawns worker fibers** as background tasks -3. **Each worker processes tasks** independently -4. **Signals Latch** when work completes (countDown) -5. **Coordinator awaits** until all workers signal -6. **Aggregates results** from all workers - ---- - -### Explanation - -Multi-fiber coordination requires synchronization: - -- **Parallel initialization**: Wait for all services to start before proceeding -- **Fan-out/fan-in**: Spawn multiple workers, collect results when all done -- **Barrier synchronization**: All fibers wait at a checkpoint before proceeding -- **Graceful shutdown**: Wait for all active fibers to complete -- **Aggregation patterns**: Process streams in parallel, combine when ready - -Unlike `Deferred` (one producer signals once), `Latch`: - -- Supports multiple signalers (each `countDown()`) -- Used with known count of participants (countdown from N to 0) -- Enables barrier patterns (all wait for all) -- Fair queuing of waiting fibers - ---- - ---- - -## Concurrency Pattern 4: Distribute Work with Queue - -**Rule:** Use Queue to distribute work between producers and consumers with built-in backpressure, enabling flexible pipeline coordination. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates a producer-consumer pipeline with a bounded queue for buffering work items. - -```typescript -import { Effect, Queue, Fiber, Ref } from "effect"; - -interface WorkItem { - readonly id: number; - readonly data: string; - readonly timestamp: number; -} - -interface WorkResult { - readonly itemId: number; - readonly processed: string; - readonly duration: number; -} - -// Producer: generates work items -const producer = ( - queue: Queue.Enqueue, - count: number -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[PRODUCER] Starting, generating ${count} items`); - - for (let i = 1; i <= count; i++) { - const item: WorkItem = { - id: i, - data: `Item ${i}`, - timestamp: Date.now(), - }; - - const start = Date.now(); - - // Enqueue - will block if queue is full (backpressure) - yield* Queue.offer(queue, item); - - const delay = Date.now() - start; - - if (delay > 0) { - yield* Effect.log( - `[PRODUCER] Item ${i} enqueued (waited ${delay}ms due to backpressure)` - ); - } else { - yield* Effect.log(`[PRODUCER] Item ${i} enqueued`); - } - - // Simulate work - yield* Effect.sleep("50 millis"); - } - - yield* Effect.log(`[PRODUCER] ✓ All items enqueued`); - }); - -// Consumer: processes work items -const consumer = ( - queue: Queue.Dequeue, - consumerId: number, - results: Ref.Ref -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[CONSUMER ${consumerId}] Starting`); - - while (true) { - // Dequeue - will block if queue is empty - const item = yield* Queue.take(queue).pipe(Effect.either); - - if (item._tag === "Left") { - yield* Effect.log(`[CONSUMER ${consumerId}] Queue closed, stopping`); - return; - } - - const workItem = item.right; - const startTime = Date.now(); - - yield* Effect.log( - `[CONSUMER ${consumerId}] Processing ${workItem.data}` - ); - - // Simulate processing - yield* Effect.sleep("150 millis"); - - const duration = Date.now() - startTime; - const result: WorkResult = { - itemId: workItem.id, - processed: `${workItem.data} [processed by consumer ${consumerId}]`, - duration, - }; - - yield* Ref.update(results, (rs) => [...rs, result]); - - yield* Effect.log( - `[CONSUMER ${consumerId}] ✓ Completed ${workItem.data} in ${duration}ms` - ); - } - }); - -// Main: coordinate producer and consumers -const program = Effect.gen(function* () { - // Create bounded queue with capacity 3 - const queue = yield* Queue.bounded(3); - const results = yield* Ref.make([]); - - console.log(`\n[MAIN] Starting producer-consumer pipeline with queue size 3\n`); - - // Spawn producer - const producerFiber = yield* producer(queue, 10).pipe(Effect.fork); - - // Spawn 2 consumers - const consumer1 = yield* consumer(queue, 1, results).pipe(Effect.fork); - const consumer2 = yield* consumer(queue, 2, results).pipe(Effect.fork); - - // Wait for producer to finish - yield* Fiber.join(producerFiber); - - // Give consumers time to finish - yield* Effect.sleep("3 seconds"); - - // Close queue and wait for consumers - yield* Queue.shutdown(queue); - yield* Fiber.join(consumer1); - yield* Fiber.join(consumer2); - - // Summary - const allResults = yield* Ref.get(results); - const totalDuration = allResults.reduce((sum, r) => sum + r.duration, 0); - - console.log(`\n[SUMMARY]`); - console.log(` Items processed: ${allResults.length}`); - console.log( - ` Avg processing time: ${Math.round(totalDuration / allResults.length)}ms` - ); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Creates bounded queue** with capacity (backpressure point) -2. **Producer enqueues** items (blocks if full) -3. **Consumers dequeue** and process (each at own pace) -4. **Queue coordinates** flow automatically - ---- - -### Explanation - -Direct producer-consumer coordination creates problems: - -- **Blocking**: Producer waits for consumer to finish -- **Tight coupling**: Producer depends on consumer speed -- **Memory pressure**: Fast producer floods memory with results -- **No backpressure**: Downstream overload propagates upstream - -Queue solves these: - -- **Asynchronous**: Producer enqueues and continues -- **Decoupled**: Producer/consumer independent -- **Backpressure**: Producer waits when queue full (natural flow control) -- **Throughput**: Consumer processes at own pace - -Real-world example: API request handler + database writer -- **Direct**: Handler waits for DB write (blocking, slow requests) -- **Queue**: Handler enqueues write and returns immediately (responsive) - ---- - ---- - -## Concurrency Pattern 5: Broadcast Events with PubSub - -**Rule:** Use PubSub to broadcast events to multiple subscribers, enabling event-driven architectures where publishers and subscribers are loosely coupled. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates a multi-subscriber event broadcast system with independent handlers. - -```typescript -import { Effect, PubSub, Fiber, Ref } from "effect"; - -interface StateChangeEvent { - readonly id: string; - readonly oldValue: string; - readonly newValue: string; - readonly timestamp: number; -} - -interface Subscriber { - readonly name: string; - readonly events: StateChangeEvent[]; -} - -// Create subscribers that react to events -const createSubscriber = ( - name: string, - pubsub: PubSub.PubSub, - events: Ref.Ref -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[${name}] ✓ Subscribed`); - - // Get subscriber handle - const subscription = yield* PubSub.subscribe(pubsub); - - // Listen for events indefinitely - while (true) { - const event = yield* subscription.take(); - - yield* Effect.log( - `[${name}] Received event: ${event.oldValue} → ${event.newValue}` - ); - - // Simulate processing - yield* Effect.sleep("50 millis"); - - // Store event (example action) - yield* Ref.update(events, (es) => [...es, event]); - - yield* Effect.log(`[${name}] ✓ Processed event`); - } - }); - -// Publisher that broadcasts events -const publisher = ( - pubsub: PubSub.PubSub, - eventCount: number -): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[PUBLISHER] Starting, publishing ${eventCount} events`); - - for (let i = 1; i <= eventCount; i++) { - const event: StateChangeEvent = { - id: `event-${i}`, - oldValue: `state-${i - 1}`, - newValue: `state-${i}`, - timestamp: Date.now(), - }; - - // Publish to all subscribers - const size = yield* PubSub.publish(pubsub, event); - - yield* Effect.log( - `[PUBLISHER] Published event to ${size} subscribers` - ); - - // Simulate delay between events - yield* Effect.sleep("200 millis"); - } - - yield* Effect.log(`[PUBLISHER] ✓ All events published`); - }); - -// Main: coordinate publisher and multiple subscribers -const program = Effect.gen(function* () { - // Create PubSub with bounded capacity - const pubsub = yield* PubSub.bounded(5); - - // Create storage for each subscriber's events - const subscriber1Events = yield* Ref.make([]); - const subscriber2Events = yield* Ref.make([]); - const subscriber3Events = yield* Ref.make([]); - - console.log(`\n[MAIN] Starting PubSub event broadcast system\n`); - - // Subscribe 3 independent subscribers - const sub1Fiber = yield* createSubscriber( - "SUBSCRIBER-1", - pubsub, - subscriber1Events - ).pipe(Effect.fork); - - const sub2Fiber = yield* createSubscriber( - "SUBSCRIBER-2", - pubsub, - subscriber2Events - ).pipe(Effect.fork); - - const sub3Fiber = yield* createSubscriber( - "SUBSCRIBER-3", - pubsub, - subscriber3Events - ).pipe(Effect.fork); - - // Wait for subscriptions to establish - yield* Effect.sleep("100 millis"); - - // Start publisher - const publisherFiber = yield* publisher(pubsub, 5).pipe(Effect.fork); - - // Wait for publisher to finish - yield* Fiber.join(publisherFiber); - - // Wait a bit for subscribers to process last events - yield* Effect.sleep("1 second"); - - // Shut down - yield* PubSub.shutdown(pubsub); - yield* Fiber.join(sub1Fiber).pipe(Effect.catchAll(() => Effect.void)); - yield* Fiber.join(sub2Fiber).pipe(Effect.catchAll(() => Effect.void)); - yield* Fiber.join(sub3Fiber).pipe(Effect.catchAll(() => Effect.void)); - - // Print summary - const events1 = yield* Ref.get(subscriber1Events); - const events2 = yield* Ref.get(subscriber2Events); - const events3 = yield* Ref.get(subscriber3Events); - - console.log(`\n[SUMMARY]`); - console.log(` Subscriber 1 received: ${events1.length} events`); - console.log(` Subscriber 2 received: ${events2.length} events`); - console.log(` Subscriber 3 received: ${events3.length} events`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Creates PubSub** for event distribution -2. **Multiple subscribers** listen independently -3. **Publisher broadcasts** events to all -4. **Each subscriber** processes at own pace - ---- - -### Explanation - -Event distribution without PubSub creates coupling: - -- **Direct references**: Publisher calls subscribers directly (tight coupling) -- **Ordering issues**: Publisher blocks on slowest subscriber -- **Scalability**: Adding subscribers slows down publisher -- **Testing**: Hard to mock multiple subscribers - -PubSub enables: - -- **Loose coupling**: Publishers emit, subscribers listen independently -- **Parallel delivery**: All subscribers notified simultaneously -- **Scalability**: Add subscribers without affecting publisher -- **Testing**: Mock single PubSub rather than all subscribers - -Real-world example: System state changes -- **Direct**: StateManager calls UserNotifier, AuditLogger, MetricsCollector (tight coupling) -- **PubSub**: StateManager publishes `StateChanged` event; subscribers listen independently - ---- - ---- - -## Concurrency Pattern 6: Race and Timeout Competing Effects - -**Rule:** Use race to compete effects and timeout to enforce deadlines, enabling cancellation when operations exceed time limits or complete. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates racing competing effects and handling timeouts. - -```typescript -import { Effect, Fiber } from "effect"; - -interface DataSource { - readonly name: string; - readonly latencyMs: number; -} - -// Simulate fetching from different sources -const fetchFromSource = (source: DataSource): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log( - `[${source.name}] Starting fetch (latency: ${source.latencyMs}ms)` - ); - - yield* Effect.sleep(`${source.latencyMs} millis`); - - const result = `Data from ${source.name}`; - - yield* Effect.log(`[${source.name}] ✓ Completed`); - - return result; - }); - -// Main: demonstrate race patterns -const program = Effect.gen(function* () { - console.log(`\n[RACE] Competing effects with race and timeout\n`); - - // Example 1: Simple race (fastest wins) - console.log(`[1] Racing 3 data sources:\n`); - - const sources: DataSource[] = [ - { name: "Primary DC", latencyMs: 200 }, - { name: "Backup DC", latencyMs: 150 }, - { name: "Cache", latencyMs: 50 }, - ]; - - const raceResult = yield* Effect.race( - fetchFromSource(sources[0]), - Effect.race(fetchFromSource(sources[1]), fetchFromSource(sources[2])) - ); - - console.log(`\nWinner: ${raceResult}\n`); - - // Example 2: Timeout - succeed within deadline - console.log(`[2] Timeout with fast operation:\n`); - - const fastOp = fetchFromSource({ name: "Fast Op", latencyMs: 100 }).pipe( - Effect.timeout("500 millis") - ); - - const fastResult = yield* fastOp; - - console.log(`✓ Completed within timeout: ${fastResult}\n`); - - // Example 3: Timeout - exceed deadline - console.log(`[3] Timeout with slow operation:\n`); - - const slowOp = fetchFromSource({ name: "Slow Op", latencyMs: 2000 }).pipe( - Effect.timeout("500 millis"), - Effect.either - ); - - const timeoutResult = yield* slowOp; - - if (timeoutResult._tag === "Left") { - console.log(`✗ Operation timed out after 500ms\n`); - } - - // Example 4: Race with timeout fallback - console.log(`[4] Race with fallback on timeout:\n`); - - const primary = fetchFromSource({ name: "Primary", latencyMs: 300 }); - - const fallback = fetchFromSource({ name: "Fallback", latencyMs: 100 }); - - const raceWithFallback = primary.pipe( - Effect.timeout("150 millis"), - Effect.catchAll(() => { - yield* Effect.log(`[PRIMARY] Timed out, using fallback`); - - return fallback; - }) - ); - - const fallbackResult = yield* raceWithFallback; - - console.log(`Result: ${fallbackResult}\n`); - - // Example 5: Race all (collect all winners) - console.log(`[5] Race all - multiple sources:\n`); - - const raceAllResult = yield* Effect.raceAll( - sources.map((s) => - fetchFromSource(s).pipe( - Effect.map((data) => ({ source: s.name, data })) - ) - ) - ); - - console.log(`First to complete: ${raceAllResult.source}\n`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Without race/timeout, competing effects create issues: - -- **Deadlocks**: Waiting for all to complete unnecessarily -- **Hanging requests**: No deadline enforcement -- **Wasted resources**: Slow operations continue indefinitely -- **No fallback**: Can't switch to alternative on timeout - -Race/timeout enable: - -- **Fastest-wins**: Take first success -- **Deadline enforcement**: Fail after time limit -- **Resource cleanup**: Cancel slower operations -- **Fallback patterns**: Alternative if primary times out - -Real-world example: Multi-datacenter request -- **Without race**: Wait for slowest response -- **With race**: Get response from fastest datacenter - ---- - ---- - -## Conditional Branching with if, when, and cond - -**Rule:** Use combinators such as if, when, and cond to branch computations based on runtime conditions, without imperative if statements. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Stream, Option, Either } from "effect"; - -// Effect: Branch based on a condition -const effect = Effect.if(true, { - onTrue: () => Effect.succeed("yes"), - onFalse: () => Effect.succeed("no"), -}); // Effect - -// Option: Conditionally create an Option -const option = true ? Option.some("yes") : Option.none(); // Option (Some("yes")) - -// Either: Conditionally create an Either -const either = true ? Either.right("yes") : Either.left("error"); // Either (Right("yes")) - -// Stream: Conditionally emit a stream -const stream = false ? Stream.fromIterable([1, 2]) : Stream.empty; // Stream (empty) -``` - -**Explanation:** -These combinators let you branch your computation based on a boolean or predicate, without leaving the world of composable, type-safe code. -You can also use `when` to run an effect only if a condition is true, or `unless` to run it only if a condition is false. - -### Anti-Pattern - -Using imperative `if` statements to decide which effect, option, either, or stream to return, breaking composability and making error/context handling less predictable. - -### Explanation - -Declarative branching keeps your code composable, testable, and easy to reason about. -It also ensures that error handling and context propagation are preserved, and that your code remains consistent across different Effect types. - ---- - -## Conditionally Branching Workflows - -**Rule:** Use predicate-based operators like Effect.filter and Effect.if to declaratively control workflow branching. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const result = yield* handled; - yield* Effect.log(result); - return result; -}); - -Effect.runPromise(programWithLogging); -``` - ---- - -### Anti-Pattern - -Using `Effect.flatMap` with a manual `if` statement and forgetting to handle the `else` case. This is a common mistake that leads to an inferred type of `Effect`, which can cause confusing type errors downstream because the success value is lost. - -```typescript -import { Effect } from "effect"; -import { findUser, isAdmin } from "./somewhere"; // From previous example - -// ❌ WRONG: The `else` case is missing. -const program = (id: number) => - findUser(id).pipe( - Effect.flatMap((user) => { - if (isAdmin(user)) { - // This returns Effect, but what happens if the user is not an admin? - return Effect.succeed(user); - } - // Because there's no `else` branch, TypeScript infers that this - // block can also implicitly return `void`. - // The resulting type is Effect, which is problematic. - }), - // This `map` will now have a type error because `u` could be `void`. - Effect.map((u) => `Welcome, ${u.name}!`) - ); - -// `Effect.filterOrFail` avoids this problem entirely by forcing a failure, -// which keeps the success channel clean and correctly typed. -``` - -### Why This is Better - -- **It's a Real Bug:** This isn't just a style issue; it's a legitimate logical error that leads to incorrect types and broken code. -- **It's a Common Mistake:** Developers new to functional pipelines often forget that every path must return a value. -- **It Reinforces the "Why":** It perfectly demonstrates _why_ `Effect.filterOrFail` is superior: `filterOrFail` guarantees that if the condition fails, the computation fails, preserving the integrity of the success channel. - -### Explanation - -This pattern allows you to embed decision-making logic directly into your composition pipelines, making your code more declarative and readable. It solves two key problems: - -1. **Separation of Concerns:** It cleanly separates the logic of producing a value from the logic of validating or making decisions about that value. -2. **Reusable Business Logic:** A predicate function (e.g., `const isAdmin = (user: User) => ...`) becomes a named, reusable, and testable piece of business logic, far superior to scattering inline `if` statements throughout your code. - -Using these operators turns conditional logic into a composable part of your `Effect`, rather than an imperative statement that breaks the flow. - ---- - ---- - -## Configure CORS for APIs - -**Rule:** Configure CORS headers to allow legitimate cross-origin requests while blocking unauthorized ones. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good Example - -```typescript -import { Effect } from "effect" -import { HttpServerRequest, HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. CORS configuration -// ============================================ - -interface CorsConfig { - readonly allowedOrigins: ReadonlyArray | "*" - readonly allowedMethods: ReadonlyArray - readonly allowedHeaders: ReadonlyArray - readonly exposedHeaders?: ReadonlyArray - readonly credentials?: boolean - readonly maxAge?: number -} - -const defaultCorsConfig: CorsConfig = { - allowedOrigins: "*", - allowedMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"], - exposedHeaders: ["X-Request-Id", "X-Response-Time"], - credentials: false, - maxAge: 86400, // 24 hours -} - -// ============================================ -// 2. Check if origin is allowed -// ============================================ - -const isOriginAllowed = ( - origin: string | undefined, - allowedOrigins: ReadonlyArray | "*" -): boolean => { - if (!origin) return false - if (allowedOrigins === "*") return true - return allowedOrigins.includes(origin) -} - -// ============================================ -// 3. Add CORS headers to response -// ============================================ - -const addCorsHeaders = ( - response: HttpServerResponse.HttpServerResponse, - origin: string | undefined, - config: CorsConfig -): HttpServerResponse.HttpServerResponse => { - let result = response - - // Set allowed origin - if (config.allowedOrigins === "*") { - result = HttpServerResponse.setHeader(result, "Access-Control-Allow-Origin", "*") - } else if (origin && isOriginAllowed(origin, config.allowedOrigins)) { - result = HttpServerResponse.setHeader(result, "Access-Control-Allow-Origin", origin) - result = HttpServerResponse.setHeader(result, "Vary", "Origin") - } - - // Set allowed methods - result = HttpServerResponse.setHeader( - result, - "Access-Control-Allow-Methods", - config.allowedMethods.join(", ") - ) - - // Set allowed headers - result = HttpServerResponse.setHeader( - result, - "Access-Control-Allow-Headers", - config.allowedHeaders.join(", ") - ) - - // Set exposed headers - if (config.exposedHeaders?.length) { - result = HttpServerResponse.setHeader( - result, - "Access-Control-Expose-Headers", - config.exposedHeaders.join(", ") - ) - } - - // Set credentials - if (config.credentials) { - result = HttpServerResponse.setHeader( - result, - "Access-Control-Allow-Credentials", - "true" - ) - } - - // Set max age for preflight cache - if (config.maxAge) { - result = HttpServerResponse.setHeader( - result, - "Access-Control-Max-Age", - String(config.maxAge) - ) - } - - return result -} - -// ============================================ -// 4. CORS middleware -// ============================================ - -const withCors = (config: CorsConfig = defaultCorsConfig) => - ( - handler: Effect.Effect - ): Effect.Effect< - HttpServerResponse.HttpServerResponse, - E, - R | HttpServerRequest.HttpServerRequest - > => - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - const origin = request.headers["origin"] - - // Handle preflight OPTIONS request - if (request.method === "OPTIONS") { - const preflightResponse = HttpServerResponse.empty({ status: 204 }) - return addCorsHeaders(preflightResponse, origin, config) - } - - // Check if origin is allowed - if ( - origin && - config.allowedOrigins !== "*" && - !isOriginAllowed(origin, config.allowedOrigins) - ) { - return HttpServerResponse.json( - { error: "CORS: Origin not allowed" }, - { status: 403 } - ) - } - - // Process request and add CORS headers to response - const response = yield* handler - return addCorsHeaders(response, origin, config) - }) - -// ============================================ -// 5. Usage examples -// ============================================ - -// Allow all origins (development) -const devCors = withCors({ - ...defaultCorsConfig, - allowedOrigins: "*", -}) - -// Specific origins (production) -const prodCors = withCors({ - allowedOrigins: [ - "https://myapp.com", - "https://admin.myapp.com", - ], - allowedMethods: ["GET", "POST", "PUT", "DELETE"], - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, - maxAge: 3600, -}) - -// Apply to handlers -const myHandler = Effect.succeed( - HttpServerResponse.json({ message: "Hello!" }) -) - -const corsEnabledHandler = devCors(myHandler) -``` - -### Explanation - -Browsers block cross-origin requests by default: - -1. **Security** - Prevents malicious sites from accessing your API -2. **Controlled access** - Allow specific origins only -3. **Credentials** - Control cookie/auth header sharing -4. **Methods** - Limit which HTTP methods are allowed - ---- - ---- - -## Configure Linting for Effect - -**Rule:** Use Biome for fast linting with Effect-friendly configuration. - -**Skill Level:** intermediate - -**Use Cases:** tooling-and-debugging - -### Good Example - -### 1. Biome Configuration (Recommended) - -```json -// biome.json -{ - "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "complexity": { - "noExcessiveCognitiveComplexity": "warn", - "noForEach": "off", // Effect uses forEach patterns - "useLiteralKeys": "off" // Effect uses computed keys - }, - "correctness": { - "noUnusedVariables": "error", - "noUnusedImports": "error", - "useExhaustiveDependencies": "warn" - }, - "style": { - "noNonNullAssertion": "warn", - "useConst": "error", - "noParameterAssign": "error" - }, - "suspicious": { - "noExplicitAny": "warn", - "noConfusingVoidType": "off" // Effect uses void - }, - "nursery": { - "noRestrictedImports": { - "level": "error", - "options": { - "paths": { - "lodash": "Use Effect functions instead", - "ramda": "Use Effect functions instead" - } - } - } - } - } - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 100 - }, - "javascript": { - "formatter": { - "semicolons": "asNeeded", - "quoteStyle": "double", - "trailingComma": "es5" - } - }, - "files": { - "ignore": [ - "node_modules", - "dist", - "coverage", - "*.gen.ts" - ] - } -} -``` - -### 2. ESLint Configuration (Alternative) - -```javascript -// eslint.config.js -import eslint from "@eslint/js" -import tseslint from "typescript-eslint" - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.strictTypeChecked, - { - languageOptions: { - parserOptions: { - project: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - rules: { - // TypeScript strict rules - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_" } - ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-floating-promises": "error", - - // Effect-friendly rules - "@typescript-eslint/no-confusing-void-expression": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { checksVoidReturn: false } - ], - - // Style rules - "prefer-const": "error", - "no-var": "error", - "object-shorthand": "error", - "prefer-template": "error", - }, - }, - { - files: ["**/*.test.ts"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - }, - }, - { - ignores: ["dist/", "coverage/", "node_modules/"], - } -) -``` - -### 3. Package.json Scripts - -```json -{ - "scripts": { - "lint": "biome check .", - "lint:fix": "biome check --apply .", - "lint:ci": "biome ci .", - "format": "biome format --write .", - "format:check": "biome format ." - } -} -``` - -### 4. VS Code Integration - -```json -// .vscode/settings.json -{ - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - } -} -``` - -### 5. Pre-commit Hook - -```json -// package.json -{ - "scripts": { - "prepare": "husky" - } -} -``` - -```bash -# .husky/pre-commit -bun run lint:ci -bun run typecheck -``` - -### 6. Effect-Specific Rules to Consider - -```typescript -// Custom rules you might want - -// ❌ Bad: Using Promise where Effect should be used -const fetchData = async () => { } // Warn in Effect codebase - -// ✅ Good: Using Effect -const fetchData = Effect.gen(function* () { }) - -// ❌ Bad: Throwing errors -const validate = (x: unknown) => { - if (!x) throw new Error("Invalid") // Error -} - -// ✅ Good: Returning Effect with error -const validate = (x: unknown) => - x ? Effect.succeed(x) : Effect.fail(new ValidationError()) - -// ❌ Bad: Using null/undefined directly -const maybeValue: string | null = null // Warn - -// ✅ Good: Using Option -const maybeValue: Option.Option = Option.none() -``` - -### Explanation - -Good linting for Effect: - -1. **Catches errors** - Unused variables, missing awaits -2. **Enforces style** - Consistent code across team -3. **Avoids antipatterns** - No implicit any, proper typing -4. **Fast feedback** - Errors in editor immediately - ---- - ---- - -## Control Flow with Conditional Combinators - -**Rule:** Use conditional combinators for control flow. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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. - -### Anti-Pattern - -Using `Effect.gen` for a single, simple conditional check can be more verbose -than necessary. For simple branching, `Effect.if` is often more concise. - -### Explanation - -These combinators allow you to embed conditional logic directly into your -`.pipe()` compositions, maintaining a declarative style for simple branching. - ---- - -## Control Repetition with Schedule - -**Rule:** Use Schedule to create composable policies for controlling the repetition and retrying of effects. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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); -``` - ---- - -### Anti-Pattern - -Writing manual, imperative retry logic. This is verbose, stateful, hard to reason about, and not easily composable. - -```typescript -import { Effect } from "effect"; -import { flakyEffect } from "./somewhere"; - -// ❌ WRONG: Manual, stateful, and complex retry logic. -function manualRetry( - effect: typeof flakyEffect, - retriesLeft: number, - delay: number -): Effect.Effect { - return effect.pipe( - Effect.catchTag("ApiError", () => { - if (retriesLeft > 0) { - return Effect.sleep(delay).pipe( - Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2)) - ); - } - return Effect.fail("ApiError" as const); - }) - ); -} - -const program = manualRetry(flakyEffect, 5, 100); -``` - -### Explanation - -While you could write manual loops or recursive functions, `Schedule` provides a much more powerful, declarative, and composable way to manage repetition. The key benefits are: - -- **Declarative:** You separate the _what_ (the effect to run) from the _how_ and _when_ (the schedule it runs on). -- **Composable:** You can build complex schedules from simple, primitive ones. For example, you can create a schedule that runs "up to 5 times, with an exponential backoff, plus some random jitter" by composing `Schedule.recurs`, `Schedule.exponential`, and `Schedule.jittered`. -- **Stateful:** A `Schedule` keeps track of its own state (like the number of repetitions), making it easy to create policies that depend on the execution history. - ---- - ---- - -## Converting from Nullable, Option, or Either - -**Rule:** Use fromNullable, fromOption, and fromEither to lift nullable values, Option, or Either into Effects or Streams for safe, typeful interop. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Option, Either } from "effect"; - -// Option: Convert a nullable value to an Option -const nullableValue: string | null = Math.random() > 0.5 ? "hello" : null; -const option = Option.fromNullable(nullableValue); // Option - -// Effect: Convert an Option to an Effect that may fail -const someValue = Option.some(42); -const effectFromOption = Option.match(someValue, { - onNone: () => Effect.fail("No value"), - onSome: (value) => Effect.succeed(value), -}); // Effect - -// Effect: Convert an Either to an Effect -const either = Either.right("success"); -const effectFromEither = Either.match(either, { - onLeft: (error) => Effect.fail(error), - onRight: (value) => Effect.succeed(value), -}); // Effect -``` - -**Explanation:** - -- `Effect.fromNullable` lifts a nullable value into an Effect, failing if the value is `null` or `undefined`. -- `Effect.fromOption` lifts an Option into an Effect, failing if the Option is `none`. -- `Effect.fromEither` lifts an Either into an Effect, failing if the Either is `left`. - -### Anti-Pattern - -Passing around `null`, `undefined`, or custom option/either types without converting them, which leads to unsafe, non-composable code and harder error handling. - -### Explanation - -Converting to Effect, Stream, Option, or Either lets you use all the combinators, error handling, and resource safety of the Effect ecosystem, while avoiding the pitfalls of `null` and `undefined`. - ---- - -## Create a Basic HTTP Server - -**Rule:** Use Http.server.serve with a platform-specific layer to run an HTTP application. - -**Skill Level:** beginner - -**Use Cases:** building-apis - -### Good Example - -This example creates a minimal server that responds to all requests with "Hello, World!". The application logic is a simple `Effect` that returns an `Http.response`. We use `NodeRuntime.runMain` to execute the server effect, which is the standard way to launch a long-running application. - -```typescript -import { Effect, Duration } from "effect"; -import * as http from "http"; - -// Create HTTP server service -class HttpServer extends Effect.Service()("HttpServer", { - sync: () => ({ - start: () => - Effect.gen(function* () { - const server = http.createServer( - (req: http.IncomingMessage, res: http.ServerResponse) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello, World!"); - } - ); - - // Add cleanup finalizer - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - yield* Effect.sync(() => server.close()); - yield* Effect.logInfo("Server shut down"); - }) - ); - - // Start server with timeout - yield* Effect.async((resume) => { - server.on("error", (error) => resume(Effect.fail(error))); - server.listen(3456, "localhost", () => { - resume(Effect.succeed(void 0)); - }); - }).pipe( - Effect.timeout(Duration.seconds(5)), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Failed to start server: ${error}`); - return yield* Effect.fail(error); - }) - ) - ); - - yield* Effect.logInfo("Server running at http://localhost:3456/"); - - // Run for a short duration to demonstrate the server is working - yield* Effect.sleep(Duration.seconds(3)); - yield* Effect.logInfo("Server demonstration complete"); - }), - }), -}) {} - -// 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.scoped // Ensure server is cleaned up properly -); - -// Run the server with proper error handling -const programWithErrorHandling = Effect.provide( - program, - HttpServer.Default -).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Program failed: ${error}`); - return yield* Effect.fail(error); - }) - ) -); - -Effect.runPromise(programWithErrorHandling).catch(() => { - process.exit(1); -}); - -/* -To test: -1. Server will timeout after 5 seconds if it can't start -2. Server runs on port 3456 to avoid conflicts -3. Proper cleanup on shutdown -4. Demonstrates server lifecycle: start -> run -> shutdown -*/ -``` - -### Anti-Pattern - -The common anti-pattern is to use the raw Node.js `http` module directly, outside of the Effect runtime. This approach creates a disconnect between your application logic and the server's lifecycle. - -```typescript -import * as http from "http"; - -// Manually create a server using the Node.js built-in module. -const server = http.createServer((req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello, World!"); -}); - -// Manually start the server and log the port. -const port = 3000; -server.listen(port, () => { - console.log(`Server running at http://localhost:${port}/`); -}); -``` - -This imperative approach is discouraged when building an Effect application because it forfeits all the benefits of the ecosystem. It runs outside of Effect's structured concurrency, cannot be managed by its resource-safe `Scope`, does not integrate with `Layer` for dependency injection, and requires manual error handling, making it less robust and much harder to compose with other effectful logic. - -### Explanation - -In Effect, an HTTP server is not just a side effect; it's a managed, effectful process. The `@effect/platform` package provides a platform-agnostic API for defining HTTP applications, while packages like `@effect/platform-node` provide the concrete implementation. - -The core function `Http.server.serve(app)` takes your application logic and returns an `Effect` that, when run, starts the server. This `Effect` is designed to run indefinitely, only terminating if the server crashes or is gracefully shut down. - -This approach provides several key benefits: - -1. **Lifecycle Management**: The server's lifecycle is managed by the Effect runtime. This means structured concurrency applies, ensuring graceful shutdowns and proper resource handling automatically. -2. **Integration**: The server is a first-class citizen in the Effect ecosystem. It can seamlessly access dependencies provided by `Layer`, use `Config` for configuration, and integrate with `Logger`. -3. **Platform Agnosticism**: By coding to the `Http.App` interface, your application logic remains portable across different JavaScript runtimes (Node.js, Bun, Deno) by simply swapping out the platform layer. - ---- - ---- - -## Create a Managed Runtime for Scoped Resources - -**Rule:** Create a managed runtime for scoped resources. - -**Skill Level:** advanced - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Layer } from "effect"; - -class DatabasePool extends Effect.Service()("DbPool", { - effect: Effect.gen(function* () { - yield* Effect.log("Acquiring pool"); - return { - query: () => Effect.succeed("result"), - }; - }), -}) {} - -// Create a program that uses the DatabasePool service -const program = Effect.gen(function* () { - const db = yield* DatabasePool; - yield* Effect.log("Using DB"); - yield* db.query(); -}); - -// Run the program with the service implementation -Effect.runPromise( - program.pipe(Effect.provide(DatabasePool.Default), Effect.scoped) -); -``` - -**Explanation:** -`Layer.launch` ensures that resources are acquired and released safely, even -in the event of errors or interruptions. - -### Anti-Pattern - -Do not use `Layer.toRuntime` with layers that contain scoped resources. This -will acquire the resource, but the runtime has no mechanism to release it, -leading to resource leaks. - -### Explanation - -`Layer.launch` is designed for resource safety. It acquires all resources, -provides them to your effect, and—crucially—guarantees that all registered -finalizers are executed upon completion or interruption. - ---- - -## Create a Reusable Runtime from Layers - -**Rule:** Create a reusable runtime from layers. - -**Skill Level:** advanced - -**Use Cases:** project-setup--execution - -### Good Example - -```typescript -import { Effect, Layer, Runtime } from "effect"; - -class GreeterService extends Effect.Service()("Greeter", { - sync: () => ({ - greet: (name: string) => Effect.sync(() => `Hello ${name}`), - }), -}) {} - -const runtime = Effect.runSync( - Layer.toRuntime(GreeterService.Default).pipe(Effect.scoped) -); - -// In a server, you would reuse `run` for every request. -Runtime.runPromise(runtime)(Effect.log("Hello")); -``` - -**Explanation:** -By compiling your layers into a Runtime once, you avoid rebuilding the -dependency graph for every effect execution. - -### Anti-Pattern - -For a long-running application, avoid providing layers and running an effect -in a single operation. This forces Effect to rebuild the dependency graph on -every execution. - -### Explanation - -Building the dependency graph from layers has a one-time cost. Creating a -`Runtime` once when your application starts is highly efficient for -long-running applications. - ---- - -## Create a Service Layer from a Managed Resource - -**Rule:** Provide a managed resource to the application context using `Layer.scoped`. - -**Skill Level:** intermediate - -**Use Cases:** resource-management - -### Good 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* Effect.log(`[Pool ${id}] Acquired`); - - // Setup cleanup to run when scope closes - yield* Effect.addFinalizer(() => Effect.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* Effect.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, which acts as both the service definition and its context key (Tag). The `Database.Live` layer connects this service to a concrete, lifecycle-managed implementation. When `program` asks for the `Database` service, the Effect runtime uses the `Live` layer to run the `acquire` effect once, caches the resulting `DbPool`, and injects it. The `release` effect is automatically run when the program completes. - -### Anti-Pattern - -Creating and exporting a global singleton instance of a resource. This tightly couples your application to a specific implementation, makes testing difficult, and offers no guarantees about graceful shutdown. - -```typescript -// ANTI-PATTERN: Global singleton -export const dbPool = makeDbPoolSync(); // Eagerly created, hard to test/mock - -function someBusinessLogic() { - // This function has a hidden dependency on the global dbPool - return dbPool.query("SELECT * FROM products"); -} -``` - -### Explanation - -This pattern is the key to building robust, testable, and leak-proof applications in Effect. It elevates a managed resource into a first-class service that can be used anywhere in your application. The `Effect.Service` helper simplifies defining the service's interface and context key. This approach decouples your business logic from the concrete implementation, as the logic only depends on the abstract service. The `Layer` declaratively handles the resource's entire lifecycle, ensuring it is acquired lazily, shared safely, and released automatically. - ---- - -## Create a Stream from a List - -**Rule:** Use Stream.fromIterable to begin a pipeline from an in-memory collection. - -**Skill Level:** beginner - -**Use Cases:** building-data-pipelines - -### Good Example - -This example takes a simple array of numbers, creates a stream from it, performs a transformation on each number, and then runs the stream to collect the results. - -```typescript -import { Effect, Stream, Chunk } from "effect"; - -const numbers = [1, 2, 3, 4, 5]; - -// Create a stream from the array of numbers. -const program = Stream.fromIterable(numbers).pipe( - // Perform a simple, synchronous transformation on each item. - Stream.map((n) => `Item: ${n}`), - // Run the stream and collect all the transformed items into a Chunk. - Stream.runCollect -); - -const programWithLogging = Effect.gen(function* () { - const processedItems = yield* program; - yield* Effect.log( - `Processed items: ${JSON.stringify(Chunk.toArray(processedItems))}` - ); - return processedItems; -}); - -Effect.runPromise(programWithLogging); -/* -Output: -[ 'Item: 1', 'Item: 2', 'Item: 3', 'Item: 4', 'Item: 5' ] -*/ -``` - -### Anti-Pattern - -The common alternative is to use standard array methods like `.map()` or a `for...of` loop. While perfectly fine for simple, synchronous tasks, this approach is an anti-pattern when building a _pipeline_. - -```typescript -const numbers = [1, 2, 3, 4, 5]; - -// Using Array.prototype.map -const processedItems = numbers.map((n) => `Item: ${n}`); - -console.log(processedItems); -``` - -This is an anti-pattern in the context of building a larger pipeline because: - -1. **It's Not Composable with Effects**: The result is just a new array. If the next step in your pipeline was an asynchronous database call for each item, you couldn't simply `.pipe()` the result into it. You would have to leave the synchronous world of `.map()` and start a new `Effect.forEach`, breaking the unified pipeline structure. -2. **It's Eager**: The `.map()` operation processes the entire array at once. `Stream` is lazy; it only processes items as they are requested by downstream consumers, which is far more efficient for large collections or complex transformations. - -### Explanation - -Every data pipeline needs a source. The simplest and most common source is a pre-existing list of items in memory. `Stream.fromIterable` is the bridge from standard JavaScript data structures to the powerful, composable world of Effect's `Stream`. - -This pattern is fundamental for several reasons: - -1. **Entry Point**: It's the "Hello, World!" of data pipelines, providing the easiest way to start experimenting with stream transformations. -2. **Testing**: In tests, you frequently need to simulate a data source (like a database query or API call). Creating a stream from a mock array of data is the standard way to do this, allowing you to test your pipeline's logic in isolation. -3. **Composability**: It transforms a static, eager data structure (an array) into a lazy, pull-based stream. This allows you to pipe it into the rest of the Effect ecosystem, enabling asynchronous operations, concurrency, and resource management in subsequent steps. - ---- - ---- - -## Create a Testable HTTP Client Service - -**Rule:** Define an HttpClient service with distinct Live and Test layers to enable testable API interactions. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good 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(() => - fetch(url).then((res) => res.json() as T) - ).pipe(Effect.catchAll((error) => Effect.fail(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; - }); -``` - ---- - -### Anti-Pattern - -Calling `fetch` directly from within your business logic functions. This creates a hard dependency on the global `fetch` API, making the function difficult to test and reuse. - -```typescript -import { Effect } from "effect"; - -// ❌ WRONG: This function is not easily testable. -export const getUserDirectly = (id: number) => - Effect.tryPromise({ - try: () => - fetch(`https://api.example.com/users/${id}`).then((res) => res.json()), - catch: () => "ApiError" as const, - }); -``` - -### Explanation - -Directly using `fetch` in your business logic makes it nearly impossible to test. Your tests would become slow, flaky (dependent on network conditions), and could have unintended side effects. - -By abstracting the HTTP client into a service, you decouple your application's logic from the specific implementation of how HTTP requests are made. Your business logic depends only on the abstract `HttpClient` interface. In production, you provide the `Live` layer. In tests, you provide the `Test` layer. This makes your tests fast, deterministic, and reliable. - ---- - ---- - -## Create Observability Dashboards - -**Rule:** Create focused dashboards that answer specific questions about system health. - -**Skill Level:** advanced - -**Use Cases:** observability - -### Explanation - -Good dashboards provide: - -1. **Quick health check** - See problems at a glance -2. **Trend analysis** - Spot gradual degradation -3. **Debugging aid** - Correlate metrics during incidents -4. **Capacity planning** - Forecast resource needs - ---- - ---- - -## Create Pre-resolved Effects with succeed and fail - -**Rule:** Create pre-resolved effects with succeed and fail. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Data } from "effect"; - -// Create a custom error type -class MyError extends Data.TaggedError("MyError") {} - -// Create a program that demonstrates pre-resolved effects -const program = Effect.gen(function* () { - // Success effect - yield* Effect.logInfo("Running success effect..."); - yield* Effect.gen(function* () { - const value = yield* Effect.succeed(42); - yield* Effect.logInfo(`Success value: ${value}`); - }); - - // Failure effect - yield* Effect.logInfo("\nRunning failure effect..."); - yield* Effect.gen(function* () { - // Use return yield* for effects that never succeed - return yield* Effect.fail(new MyError()); - }).pipe( - Effect.catchTag("MyError", (error) => - Effect.logInfo(`Error occurred: ${error._tag}`) - ) - ); -}); - -// Run the program -Effect.runPromise(program); -``` - -**Explanation:** -Use `Effect.succeed` for values you already have, and `Effect.fail` for -immediate, known errors. - -### Anti-Pattern - -Do not wrap a static value in `Effect.sync`. While it works, `Effect.succeed` -is more descriptive and direct for values that are already available. - -### Explanation - -These are the simplest effect constructors, essential for returning static -values within functions that must return an `Effect`. - ---- - -## Create Type-Safe Errors - -**Rule:** Use Data.TaggedError to create typed, distinguishable errors for your domain. - -**Skill Level:** beginner - -**Use Cases:** domain-modeling - -### Good Example - -```typescript -import { Effect, Data } from "effect" - -// ============================================ -// 1. Define tagged errors for your domain -// ============================================ - -class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ - readonly userId: string -}> {} - -class InvalidEmailError extends Data.TaggedError("InvalidEmailError")<{ - readonly email: string - readonly reason: string -}> {} - -class DuplicateUserError extends Data.TaggedError("DuplicateUserError")<{ - readonly email: string -}> {} - -// ============================================ -// 2. Use in Effect functions -// ============================================ - -interface User { - id: string - email: string - name: string -} - -const validateEmail = (email: string): Effect.Effect => { - if (!email.includes("@")) { - return Effect.fail(new InvalidEmailError({ - email, - reason: "Missing @ symbol" - })) - } - return Effect.succeed(email) -} - -const findUser = (id: string): Effect.Effect => { - // Simulate database lookup - if (id === "123") { - return Effect.succeed({ id, email: "alice@example.com", name: "Alice" }) - } - return Effect.fail(new UserNotFoundError({ userId: id })) -} - -const createUser = ( - email: string, - name: string -): Effect.Effect => - Effect.gen(function* () { - const validEmail = yield* validateEmail(email) - - // Simulate duplicate check - if (validEmail === "taken@example.com") { - return yield* Effect.fail(new DuplicateUserError({ email: validEmail })) - } - - return { - id: crypto.randomUUID(), - email: validEmail, - name, - } - }) - -// ============================================ -// 3. Handle errors by tag -// ============================================ - -const program = createUser("alice@example.com", "Alice").pipe( - Effect.catchTag("InvalidEmailError", (error) => - Effect.succeed({ - id: "fallback", - email: "default@example.com", - name: `${error.email} was invalid: ${error.reason}`, - }) - ), - Effect.catchTag("DuplicateUserError", (error) => - Effect.fail(new Error(`Email ${error.email} already registered`)) - ) -) - -// ============================================ -// 4. Match on all errors -// ============================================ - -const handleAllErrors = createUser("bad-email", "Bob").pipe( - Effect.catchTags({ - InvalidEmailError: (e) => Effect.succeed(`Invalid: ${e.reason}`), - DuplicateUserError: (e) => Effect.succeed(`Duplicate: ${e.email}`), - }) -) - -// ============================================ -// 5. Run and see results -// ============================================ - -Effect.runPromise(program) - .then((user) => console.log("Created:", user)) - .catch((error) => console.error("Failed:", error)) -``` - -### Explanation - -Plain `Error` or string messages cause problems: - -1. **No type safety** - Can't know what errors a function might throw -2. **Hard to handle** - Matching on error messages is fragile -3. **Poor documentation** - Errors aren't part of the function signature - -Tagged errors solve this by making errors typed and distinguishable. - ---- - ---- - -## Creating from Collections - -**Rule:** Use fromIterable and fromArray to lift collections into Streams or Effects for batch or streaming processing. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Stream, Effect } from "effect"; - -// Stream: Create a stream from an array -const numbers = [1, 2, 3, 4]; -const numberStream = Stream.fromIterable(numbers); // Stream - -// Stream: Create a stream from any iterable -function* gen() { - yield "a"; - yield "b"; -} -const letterStream = Stream.fromIterable(gen()); // Stream - -// Effect: Create an effect from an array of effects (batch) -const effects = [Effect.succeed(1), Effect.succeed(2)]; -const batchEffect = Effect.all(effects); // Effect<[1, 2]> -``` - -**Explanation:** - -- `Stream.fromIterable` creates a stream from any array or iterable, enabling streaming and batch operations. -- `Effect.all` (covered elsewhere) can be used to process arrays of effects in batch. - -### Anti-Pattern - -Manually looping over collections and running effects or streams imperatively, which loses composability, error handling, and resource safety. - -### Explanation - -Lifting collections into Streams or Effects allows you to process data in a composable, resource-safe, and potentially concurrent way. -It also enables you to use all of Effect's combinators for transformation, filtering, and error handling. - ---- - -## Creating from Synchronous and Callback Code - -**Rule:** Use sync and async to create Effects from synchronous or callback-based computations, making them composable and type-safe. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Synchronous: Wrap a computation that is guaranteed not to throw -const effectSync = Effect.sync(() => Math.random()); // Effect - -// Callback-based: Wrap a Node.js-style callback API -function legacyReadFile( - path: string, - cb: (err: Error | null, data?: string) => void -) { - setTimeout(() => cb(null, "file contents"), 10); -} - -const effectAsync = Effect.async((resume) => { - legacyReadFile("file.txt", (err, data) => { - if (err) resume(Effect.fail(err)); - else resume(Effect.succeed(data!)); - }); -}); // Effect -``` - -**Explanation:** - -- `Effect.sync` is for synchronous computations that are guaranteed not to throw. -- `Effect.async` is for integrating callback-based APIs, converting them into Effects. - -### Anti-Pattern - -Directly calling synchronous or callback-based APIs inside Effects without lifting them, which can break composability and error handling. - -### Explanation - -Many APIs are synchronous or use callbacks instead of Promises. -By lifting them into Effects, you gain access to all of Effect's combinators, error handling, and resource safety. - ---- - -## Debug Effect Programs - -**Rule:** Use Effect.tap and logging to inspect values without changing program flow. - -**Skill Level:** beginner - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, pipe } from "effect" - -// ============================================ -// 1. Using tap to inspect values -// ============================================ - -const fetchUser = (id: string) => - Effect.succeed({ id, name: "Alice", email: "alice@example.com" }) - -const processUser = (id: string) => - fetchUser(id).pipe( - // tap runs an effect for its side effect, then continues with original value - Effect.tap((user) => Effect.log(`Fetched user: ${user.name}`)), - Effect.map((user) => ({ ...user, processed: true })), - Effect.tap((user) => Effect.log(`Processed: ${JSON.stringify(user)}`)) - ) - -// ============================================ -// 2. Debug a pipeline -// ============================================ - -const numbers = [1, 2, 3, 4, 5] - -const pipeline = Effect.gen(function* () { - yield* Effect.log("Starting pipeline") - - const step1 = numbers.filter((n) => n % 2 === 0) - yield* Effect.log(`After filter (even): ${JSON.stringify(step1)}`) - - const step2 = step1.map((n) => n * 10) - yield* Effect.log(`After map (*10): ${JSON.stringify(step2)}`) - - const step3 = step2.reduce((a, b) => a + b, 0) - yield* Effect.log(`After reduce (sum): ${step3}`) - - return step3 -}) - -// ============================================ -// 3. Debug errors -// ============================================ - -const riskyOperation = (shouldFail: boolean) => - Effect.gen(function* () { - yield* Effect.log("Starting risky operation") - - if (shouldFail) { - yield* Effect.log("About to fail...") - return yield* Effect.fail(new Error("Something went wrong")) - } - - yield* Effect.log("Success!") - return "result" - }) - -const debugErrors = riskyOperation(true).pipe( - // Log when operation fails - Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)), - - // Provide a fallback - Effect.catchAll((error) => { - return Effect.succeed(`Recovered from: ${error.message}`) - }) -) - -// ============================================ -// 4. Trace execution flow -// ============================================ - -const step = (name: string, value: number) => - Effect.gen(function* () { - yield* Effect.log(`[${name}] Input: ${value}`) - const result = value * 2 - yield* Effect.log(`[${name}] Output: ${result}`) - return result - }) - -const tracedWorkflow = Effect.gen(function* () { - const a = yield* step("Step 1", 5) - const b = yield* step("Step 2", a) - const c = yield* step("Step 3", b) - yield* Effect.log(`Final result: ${c}`) - return c -}) - -// ============================================ -// 5. Quick debug with console -// ============================================ - -// Sometimes you just need console.log -const quickDebug = Effect.gen(function* () { - const value = yield* Effect.succeed(42) - - // Effect.sync wraps side effects - yield* Effect.sync(() => console.log("Quick debug:", value)) - - return value -}) - -// ============================================ -// 6. Run examples -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Tap Example ===") - yield* processUser("123") - - yield* Effect.log("\n=== Pipeline Debug ===") - yield* pipeline - - yield* Effect.log("\n=== Error Debug ===") - yield* debugErrors - - yield* Effect.log("\n=== Traced Workflow ===") - yield* tracedWorkflow -}) - -Effect.runPromise(program) -``` - -### Explanation - -Debugging Effect code differs from imperative code: - -1. **No breakpoints** - Effects are descriptions, not executions -2. **Lazy evaluation** - Code runs later when you call `runPromise` -3. **Composition** - Effects chain together - -`tap` and logging let you see inside without breaking the chain. - ---- - ---- - -## Decouple Fibers with Queues and PubSub - -**Rule:** Use Queue for point-to-point work distribution and PubSub for broadcast messaging between fibers. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -A producer fiber adds jobs to a `Queue`, and a worker fiber takes jobs off the queue to process them. - -```typescript -import { Effect, Queue, Fiber } from "effect"; - -const program = Effect.gen(function* () { - yield* Effect.logInfo("Starting queue demo..."); - - // Create a bounded queue that can hold a maximum of 10 items. - // This prevents memory issues by applying backpressure when the queue is full. - // If a producer tries to add to a full queue, it will suspend until space is available. - const queue = yield* Queue.bounded(10); - yield* Effect.logInfo("Created bounded queue"); - - // Producer Fiber: Add a job to the queue every second. - // This fiber runs independently and continuously produces work items. - // The producer-consumer pattern decouples work generation from work processing. - const producer = yield* Effect.gen(function* () { - let i = 0; - while (true) { - const job = `job-${i++}`; - yield* Effect.logInfo(`Producing ${job}...`); - - // Queue.offer adds an item to the queue. If the queue is full, - // this operation will suspend the fiber until space becomes available. - // This provides natural backpressure control. - yield* Queue.offer(queue, job); - - // Sleep for 500ms between job creation. This controls the production rate. - // Producer is faster than consumer (500ms vs 1000ms) to demonstrate queue buffering. - yield* Effect.sleep("500 millis"); - } - }).pipe(Effect.fork); // Fork creates a new fiber that runs concurrently - - yield* Effect.logInfo("Started producer fiber"); - - // Worker Fiber: Take a job from the queue and process it. - // This fiber runs independently and processes work items as they become available. - // Multiple workers could be created to scale processing capacity. - const worker = yield* Effect.gen(function* () { - while (true) { - // Queue.take removes and returns an item from the queue. - // If the queue is empty, this operation will suspend the fiber - // until an item becomes available. This prevents busy-waiting. - const job = yield* Queue.take(queue); - yield* Effect.logInfo(`Processing ${job}...`); - - // Simulate work by sleeping for 1 second. - // This makes the worker slower than the producer, causing queue buildup. - yield* Effect.sleep("1 second"); - yield* Effect.logInfo(`Completed ${job}`); - } - }).pipe(Effect.fork); // Fork creates another independent fiber - - yield* Effect.logInfo("Started worker fiber"); - - // Let them run for a while... - // The main fiber sleeps while the producer and worker fibers run concurrently. - // During this time, you'll see the queue acting as a buffer between - // the fast producer and slow worker. - yield* Effect.logInfo("Running for 10 seconds..."); - yield* Effect.sleep("10 seconds"); - yield* Effect.logInfo("Done!"); - - // Interrupt both fibers to clean up resources. - // Fiber.interrupt sends an interruption signal to the fiber, - // allowing it to perform cleanup operations before terminating. - // This is safer than forcefully killing fibers. - yield* Fiber.interrupt(producer); - yield* Fiber.interrupt(worker); - - // Note: In a real application, you might want to: - // 1. Drain the queue before interrupting workers - // 2. Use Fiber.join to wait for graceful shutdown - // 3. Handle interruption signals in the fiber loops -}); - -// Run the program -// This demonstrates the producer-consumer pattern with Effect fibers: -// - Fibers are lightweight threads that can be created in large numbers -// - Queues provide safe communication between fibers -// - Backpressure prevents resource exhaustion -// - Interruption allows for graceful shutdown -Effect.runPromise(program); -``` - - -A publisher sends an event, and multiple subscribers react to it independently. - -```typescript -import { Effect, PubSub } from "effect"; - -const program = Effect.gen(function* () { - const pubsub = yield* PubSub.bounded(10); - - // Subscriber 1: The "Audit" service - const auditSub = PubSub.subscribe(pubsub).pipe( - Effect.flatMap((subscription) => - Effect.gen(function* () { - while (true) { - const event = yield* Queue.take(subscription); - yield* Effect.log(`AUDIT: Received event: ${event}`); - } - }) - ), - Effect.fork - ); - - // Subscriber 2: The "Notifier" service - const notifierSub = PubSub.subscribe(pubsub).pipe( - Effect.flatMap((subscription) => - Effect.gen(function* () { - while (true) { - const event = yield* Queue.take(subscription); - yield* Effect.log(`NOTIFIER: Sending notification for: ${event}`); - } - }) - ), - Effect.fork - ); - - // Give subscribers time to start - yield* Effect.sleep("1 second"); - - // Publisher: Publish an event that both subscribers will receive. - yield* PubSub.publish(pubsub, "user_logged_in"); -}); -``` - ---- - -### Anti-Pattern - -Simulating a queue with a simple `Ref`. This approach is inefficient due to polling and is not safe from race conditions without manual, complex locking mechanisms. It also lacks critical features like back-pressure. - -```typescript -import { Effect, Ref } from "effect"; - -// ❌ WRONG: This is inefficient and prone to race conditions. -const program = Effect.gen(function* () { - const queueRef = yield* Ref.make([]); - - // Producer adds to the array - const producer = Ref.update(queueRef, (q) => [...q, "new_item"]); - - // Consumer has to constantly poll the array to see if it's empty. - const consumer = Ref.get(queueRef).pipe( - Effect.flatMap( - (q) => - q.length > 0 - ? Ref.set(queueRef, q.slice(1)).pipe(Effect.as(q[0])) - : Effect.sleep("1 second").pipe(Effect.flatMap(() => consumer)) // Inefficient polling - ) - ); -}); -``` - -### Explanation - -Directly calling functions between different logical parts of a concurrent application creates tight coupling, making the system brittle and hard to scale. `Queue` and `PubSub` solve this by acting as asynchronous, fiber-safe message brokers. - -This decouples the **producer** of a message from its **consumer(s)**. The producer doesn't need to know who is listening, or how many listeners there are. This allows you to build resilient, scalable systems where you can add or remove workers/listeners without changing the producer's code. - -Furthermore, bounded `Queue`s and `PubSub`s provide automatic **back-pressure**. If consumers can't keep up, the producer will automatically pause before adding new items, preventing your system from becoming overloaded. - ---- - ---- - -## Define a Type-Safe Configuration Schema - -**Rule:** Define a type-safe configuration schema. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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. - -### Anti-Pattern - -Directly accessing `process.env`. This is not type-safe, scatters configuration access throughout your codebase, and can lead to parsing errors or `undefined` values. - -### Explanation - -This creates a single, type-safe source of truth for your configuration, eliminating runtime errors from missing or malformed environment variables and making the required configuration explicit. - ---- - -## Define Contracts Upfront with Schema - -**Rule:** Define contracts upfront with schema. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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. - -### Anti-Pattern - -Defining logic with implicit `any` types first and adding validation later as -an afterthought. This leads to brittle code that lacks a clear contract. - -### Explanation - -This "schema-first" approach separates the "what" (the data shape) from the -"how" (the implementation). It provides a single source of truth for both -compile-time static types and runtime validation. - ---- - -## Define Type-Safe Errors with Data.TaggedError - -**Rule:** Define type-safe errors with Data.TaggedError. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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. - -### Anti-Pattern - -Using generic `Error` objects or strings in the error channel. This loses all -type information, forcing consumers to use `catchAll` and perform unsafe -checks. - -### Explanation - -This gives each error a unique, literal `_tag` that Effect can use for type -discrimination with `Effect.catchTag`, making your error handling fully -type-safe. - ---- - -## Distinguish 'Not Found' from Errors - -**Rule:** Use Effect> to distinguish between recoverable 'not found' cases and actual failures. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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); - }) -); -``` - -### Anti-Pattern - -A common alternative is to create a specific NotFoundError and put it in the error channel alongside other errors. - -```typescript -class NotFoundError extends Data.TaggedError("NotFoundError") {} - -// ❌ This signature conflates two different kinds of failure. -const findUserUnsafely = ( - id: number -): Effect.Effect => { - // ... - return Effect.fail(new NotFoundError()); -}; -``` - -While this works, it can be less expressive. It treats a "not found" result—which might be a normal part of your application's flow—the same as a catastrophic DatabaseError. - -Using `Effect>` often leads to clearer and more precise business logic. - -### Explanation - -This pattern provides a precise way to handle three distinct outcomes of an operation: - -1. **Success with a value:** `Effect.succeed(Option.some(value))` -2. **Success with no value:** `Effect.succeed(Option.none())` (e.g., user not found) -3. **Failure:** `Effect.fail(new DatabaseError())` (e.g., database connection lost) - -By using `Option` inside the success channel of an `Effect`, you keep the error channel clean for true, unexpected, or unrecoverable errors. The "not found" case is often an expected and recoverable part of your business logic, and `Option.none()` models this perfectly. - ---- - ---- - -## Effectful Pattern Matching with matchEffect - -**Rule:** Use matchEffect to pattern match on the result of an Effect, running effectful logic for both success and failure cases. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Effect: Run different Effects on success or failure -const effect = Effect.fail("Oops!").pipe( - Effect.matchEffect({ - onFailure: (err) => Effect.logError(`Error: ${err}`), - onSuccess: (value) => Effect.log(`Success: ${value}`), - }) -); // Effect -``` - -**Explanation:** - -- `matchEffect` allows you to run an Effect for both the success and failure cases. -- This is useful for logging, cleanup, retries, or any effectful side effect that depends on the outcome. - -### Anti-Pattern - -Using `match` to return values and then wrapping them in Effects, or duplicating logic for side effects, instead of using `matchEffect` for direct effectful branching. - -### Explanation - -Sometimes, handling a success or failure requires running additional Effects (e.g., logging, retries, cleanup). -`matchEffect` lets you do this declaratively, keeping your code composable and type-safe. - ---- - -## Error Handling Pattern 1: Accumulating Multiple Errors - -**Rule:** Use error accumulation to report all problems at once rather than failing early, critical for validation and batch operations. - -**Skill Level:** intermediate - -**Use Cases:** error-handling - -### Good Example - -This example demonstrates error accumulation patterns. - -```typescript -import { Effect, Data, Cause } from "effect"; - -interface ValidationError { - field: string; - message: string; - value?: unknown; -} - -interface ProcessingResult { - successes: T[]; - errors: ValidationError[]; -} - -// Example 1: Form validation with error accumulation -const program = Effect.gen(function* () { - console.log(`\n[ERROR ACCUMULATION] Collecting multiple errors\n`); - - // Form data - interface FormData { - name: string; - email: string; - age: number; - phone: string; - } - - const validateForm = (data: FormData): ValidationError[] => { - const errors: ValidationError[] = []; - - // Validation 1: Name - if (!data.name || data.name.trim().length === 0) { - errors.push({ - field: "name", - message: "Name is required", - value: data.name, - }); - } else if (data.name.length < 2) { - errors.push({ - field: "name", - message: "Name must be at least 2 characters", - value: data.name, - }); - } - - // Validation 2: Email - if (!data.email) { - errors.push({ - field: "email", - message: "Email is required", - value: data.email, - }); - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) { - errors.push({ - field: "email", - message: "Email format invalid", - value: data.email, - }); - } - - // Validation 3: Age - if (data.age < 0 || data.age > 150) { - errors.push({ - field: "age", - message: "Age must be between 0 and 150", - value: data.age, - }); - } - - // Validation 4: Phone - if (data.phone && !/^\d{3}-\d{3}-\d{4}$/.test(data.phone)) { - errors.push({ - field: "phone", - message: "Phone must be in format XXX-XXX-XXXX", - value: data.phone, - }); - } - - return errors; - }; - - // Example 1: Form with multiple errors - console.log(`[1] Form validation with multiple errors:\n`); - - const invalidForm: FormData = { - name: "", - email: "not-an-email", - age: 200, - phone: "invalid", - }; - - const validationErrors = validateForm(invalidForm); - - yield* Effect.log(`[VALIDATION] Found ${validationErrors.length} errors:\n`); - - for (const error of validationErrors) { - yield* Effect.log(` ✗ ${error.field}: ${error.message}`); - } - - // Example 2: Batch processing with partial success - console.log(`\n[2] Batch processing (accumulate successes and failures):\n`); - - interface Record { - id: string; - data: string; - } - - const processRecord = (record: Record): Result => { - if (record.id.length === 0) { - return { success: false, error: "Missing ID" }; - } - - if (record.data.includes("ERROR")) { - return { success: false, error: "Invalid data" }; - } - - return { success: true, value: `processed-${record.id}` }; - }; - - interface Result { - success: boolean; - value?: T; - error?: string; - } - - const records: Record[] = [ - { id: "rec1", data: "ok" }, - { id: "", data: "ok" }, // Error: missing ID - { id: "rec3", data: "ok" }, - { id: "rec4", data: "ERROR" }, // Error: invalid data - { id: "rec5", data: "ok" }, - ]; - - const results: ProcessingResult = { - successes: [], - errors: [], - }; - - for (const record of records) { - const result = processRecord(record); - - if (result.success) { - results.successes.push(result.value!); - } else { - results.errors.push({ - field: record.id || "unknown", - message: result.error!, - }); - } - } - - yield* Effect.log( - `[BATCH] Processed ${records.length} records` - ); - yield* Effect.log(`[BATCH] ✓ ${results.successes.length} succeeded`); - yield* Effect.log(`[BATCH] ✗ ${results.errors.length} failed\n`); - - for (const success of results.successes) { - yield* Effect.log(` ✓ ${success}`); - } - - for (const error of results.errors) { - yield* Effect.log(` ✗ [${error.field}] ${error.message}`); - } - - // Example 3: Multi-step validation with error accumulation - console.log(`\n[3] Multi-step validation (all checks run):\n`); - - interface ServiceHealth { - diskSpace: boolean; - memory: boolean; - network: boolean; - database: boolean; - } - - const diagnostics: ValidationError[] = []; - - // Check 1: Disk space - const diskFree = 50; // MB - - if (diskFree < 100) { - diagnostics.push({ - field: "disk-space", - message: `Only ${diskFree}MB free (need 100MB)`, - value: diskFree, - }); - } - - // Check 2: Memory - const memUsage = 95; // percent - - if (memUsage > 85) { - diagnostics.push({ - field: "memory", - message: `Using ${memUsage}% (threshold: 85%)`, - value: memUsage, - }); - } - - // Check 3: Network - const latency = 500; // ms - - if (latency > 200) { - diagnostics.push({ - field: "network", - message: `Latency ${latency}ms (threshold: 200ms)`, - value: latency, - }); - } - - // Check 4: Database - const dbConnections = 95; - const dbMax = 100; - - if (dbConnections > dbMax * 0.8) { - diagnostics.push({ - field: "database", - message: `${dbConnections}/${dbMax} connections (80% threshold)`, - value: dbConnections, - }); - } - - if (diagnostics.length === 0) { - yield* Effect.log(`[HEALTH] ✓ All systems normal\n`); - } else { - yield* Effect.log( - `[HEALTH] ✗ ${diagnostics.length} issue(s) detected:\n` - ); - - for (const diag of diagnostics) { - yield* Effect.log(` ⚠ ${diag.field}: ${diag.message}`); - } - } - - // Example 4: Error collection with retry decisions - console.log(`\n[4] Error collection for retry strategy:\n`); - - interface ErrorWithContext { - operation: string; - error: string; - retryable: boolean; - timestamp: Date; - } - - const operationErrors: ErrorWithContext[] = []; - - const operations = [ - { name: "fetch-config", fail: false }, - { name: "connect-db", fail: true }, - { name: "load-cache", fail: true }, - { name: "start-server", fail: false }, - ]; - - for (const op of operations) { - if (op.fail) { - operationErrors.push({ - operation: op.name, - error: "Operation failed", - retryable: op.name !== "fetch-config", - timestamp: new Date(), - }); - } - } - - yield* Effect.log(`[OPERATIONS] ${operationErrors.length} errors:\n`); - - for (const err of operationErrors) { - const status = err.retryable ? "🔄 retryable" : "❌ non-retryable"; - yield* Effect.log(` ${status}: ${err.operation}`); - } - - if (operationErrors.every((e) => e.retryable)) { - yield* Effect.log(`\n[DECISION] All errors retryable, will retry\n`); - } else { - yield* Effect.log(`\n[DECISION] Some non-retryable errors, manual intervention needed\n`); - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Failing fast causes problems: - -**Problem 1: Form validation** -- User submits form with 10 field errors -- Fail on first error: "Name required" -- User fixes name, submits again -- New error: "Email invalid" -- User submits 10 times before fixing all errors -- Frustration, reduced productivity - -**Problem 2: Batch processing** -- Process 1000 records, fail on record 5 -- 995 records not processed -- User manually retries -- Repeats for each error type -- Inefficient - -**Problem 3: System diagnostics** -- Service health check fails -- Report: "Check 1 failed" -- Fix check 1, service still down -- Hidden problem: checks 2, 3, and 4 also failed -- Time wasted diagnosing - -Solutions: - -**Error accumulation**: -- Run all validations -- Collect errors -- Report all problems -- User fixes once, not 10 times - -**Partial success**: -- Process all records -- Track successes and failures -- Return: "950 succeeded, 50 failed" -- No re-processing - -**Comprehensive diagnostics**: -- Run all checks -- Report all failures -- Quick root cause analysis -- Faster resolution - ---- - ---- - -## Error Handling Pattern 2: Error Propagation and Chains - -**Rule:** Use error propagation to preserve context through effect chains, enabling debugging and recovery at the right abstraction level. - -**Skill Level:** advanced - -**Use Cases:** error-handling - -### Good Example - -This example demonstrates error propagation with context. - -```typescript -import { Effect, Data, Cause } from "effect"; - -// Domain-specific errors with context -class DatabaseError extends Data.TaggedError("DatabaseError")<{ - query: string; - parameters: unknown[]; - cause: Error; -}> {} - -class NetworkError extends Data.TaggedError("NetworkError")<{ - endpoint: string; - method: string; - statusCode?: number; - cause: Error; -}> {} - -class ValidationError extends Data.TaggedError("ValidationError")<{ - field: string; - value: unknown; - reason: string; -}> {} - -class BusinessLogicError extends Data.TaggedError("BusinessLogicError")<{ - operation: string; - context: Record; - originalError: Error; -}> {} - -const program = Effect.gen(function* () { - console.log(`\n[ERROR PROPAGATION] Error chains with context\n`); - - // Example 1: Simple error propagation - console.log(`[1] Error propagation through layers:\n`); - - const lowLevelOperation = Effect.gen(function* () { - yield* Effect.log(`[LAYER 1] Low-level operation starting`); - - yield* Effect.fail(new Error("File not found")); - }); - - const midLevelOperation = lowLevelOperation.pipe( - Effect.mapError((error) => - new DatabaseError({ - query: "SELECT * FROM users", - parameters: ["id=123"], - cause: error instanceof Error ? error : new Error(String(error)), - }) - ) - ); - - const highLevelOperation = midLevelOperation.pipe( - Effect.catchTag("DatabaseError", (dbError) => - Effect.gen(function* () { - yield* Effect.log(`[LAYER 3] Caught database error`); - yield* Effect.log(`[LAYER 3] Query: ${dbError.query}`); - yield* Effect.log(`[LAYER 3] Cause: ${dbError.cause.message}`); - - // Recovery decision - return "fallback-value"; - }) - ) - ); - - const result1 = yield* highLevelOperation; - - yield* Effect.log(`[RESULT] Recovered with: ${result1}\n`); - - // Example 2: Error context accumulation - console.log(`[2] Accumulating context through layers:\n`); - - interface ErrorContext { - timestamp: Date; - operation: string; - userId?: string; - requestId: string; - } - - const errorWithContext = (context: ErrorContext) => - Effect.fail( - new BusinessLogicError({ - operation: context.operation, - context: { - userId: context.userId, - timestamp: context.timestamp.toISOString(), - requestId: context.requestId, - }, - originalError: new Error("Operation failed"), - }) - ); - - const myContext: ErrorContext = { - timestamp: new Date(), - operation: "process-payment", - userId: "user-123", - requestId: "req-abc-def", - }; - - const withContextRecovery = errorWithContext(myContext).pipe( - Effect.mapError((error) => { - // Log complete context - return { - ...error, - enriched: true, - additionalInfo: { - serviceName: "payment-service", - environment: "production", - version: "1.2.3", - }, - }; - }), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[ERROR CAUGHT] ${error.operation}`); - yield* Effect.log(`[CONTEXT] ${JSON.stringify(error.context, null, 2)}`); - return "recovered"; - }) - ) - ); - - yield* withContextRecovery; - - // Example 3: Network error with retry context - console.log(`\n[3] Network errors with retry context:\n`); - - interface RetryContext { - attempt: number; - maxAttempts: number; - delay: number; - } - - let attemptCount = 0; - - const networkCall = Effect.gen(function* () { - attemptCount++; - - yield* Effect.log(`[ATTEMPT] ${attemptCount}/3`); - - if (attemptCount < 3) { - yield* Effect.fail( - new NetworkError({ - endpoint: "https://api.example.com/data", - method: "GET", - statusCode: 503, - cause: new Error("Service Unavailable"), - }) - ); - } - - return "success"; - }); - - const withRetryContext = Effect.gen(function* () { - let lastError: NetworkError | null = null; - - for (let i = 1; i <= 3; i++) { - const result = yield* networkCall.pipe( - Effect.catchTag("NetworkError", (error) => { - lastError = error; - - yield* Effect.log( - `[RETRY] Attempt ${i} failed: ${error.statusCode}` - ); - - if (i < 3) { - yield* Effect.log(`[RETRY] Waiting before retry...`); - } - - return Effect.fail(error); - }) - ).pipe( - Effect.tap(() => Effect.log(`[SUCCESS] Connected on attempt ${i}`)) - ).pipe( - Effect.catchAll(() => Effect.succeed(null)) - ); - - if (result !== null) { - return result; - } - } - - if (lastError) { - yield* Effect.fail(lastError); - } - - return null; - }); - - const networkResult = yield* withRetryContext.pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[EXHAUSTED] All retries failed`); - return "fallback"; - }) - ) - ); - - yield* Effect.log(`\n`); - - // Example 4: Multi-layer error transformation - console.log(`[4] Error transformation between layers:\n`); - - const layer1Error = Effect.gen(function* () { - yield* Effect.fail(new Error("Raw system error")); - }); - - // Layer 2: Convert to domain error - const layer2 = layer1Error.pipe( - Effect.mapError((error) => - new DatabaseError({ - query: "SELECT ...", - parameters: [], - cause: error instanceof Error ? error : new Error(String(error)), - }) - ) - ); - - // Layer 3: Convert to business error - const layer3 = layer2.pipe( - Effect.mapError((dbError) => - new BusinessLogicError({ - operation: "fetch-user-profile", - context: { - dbError: dbError.query, - }, - originalError: dbError.cause, - }) - ) - ); - - // Layer 4: Return user-friendly error - const userFacingError = layer3.pipe( - Effect.mapError((bizError) => ({ - message: "Unable to load profile", - code: "PROFILE_LOAD_FAILED", - originalError: bizError.originalError.message, - })), - Effect.catchAll((userError) => - Effect.gen(function* () { - yield* Effect.log(`[USER MESSAGE] ${userError.message}`); - yield* Effect.log(`[CODE] ${userError.code}`); - yield* Effect.log(`[DEBUG] ${userError.originalError}`); - return null; - }) - ) - ); - - yield* userFacingError; - - // Example 5: Error aggregation in concurrent operations - console.log(`\n[5] Error propagation in concurrent operations:\n`); - - const operation = (id: number, shouldFail: boolean) => - Effect.gen(function* () { - if (shouldFail) { - yield* Effect.fail( - new Error(`Operation ${id} failed`) - ); - } - - return `result-${id}`; - }); - - const concurrent = Effect.gen(function* () { - const results = yield* Effect.all( - [ - operation(1, false), - operation(2, true), - operation(3, false), - ], - { concurrency: 3 } - ).pipe( - Effect.catchAll((errors) => - Effect.gen(function* () { - yield* Effect.log(`[CONCURRENT] Caught aggregated errors`); - - // In real code, Cause provides error details - yield* Effect.log(`[ERROR] Errors encountered during concurrent execution`); - - return []; - }) - ) - ); - - return results; - }); - - yield* concurrent; - - yield* Effect.log(`\n[DEMO] Error propagation complete`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Loss of error context causes problems: - -**Problem 1: Useless error messages** -- User sees: "Error: null" -- Debugging: Where did it come from? When? Why? -- Wasted hours searching logs - -**Problem 2: Wrong recovery layer** -- Network error → recovered at business logic layer (inefficient) -- Should be recovered at network layer → retry, exponential backoff - -**Problem 3: Error context loss** -- Database connection failed -- But which database? Which query? With what parameters? -- Logs show "Connection failed" (not actionable) - -**Problem 4: Hidden root cause** -- Effect 1 fails → triggers Effect 2 → different error -- Developer sees Effect 2 error -- Doesn't know Effect 1 was root cause -- Fixes wrong thing - -Solutions: - -**Error context**: -- Include operation name -- Include relevant parameters -- Include timestamps -- Include retry count - -**Error cause chains**: -- Keep original error -- Add context at each layer -- `mapError()` to transform -- `tapError()` to log context - -**Recovery layers**: -- Low-level: Retry network requests -- Mid-level: Transform domain errors -- High-level: Convert to user-friendly messages - ---- - ---- - -## Error Handling Pattern 3: Custom Error Strategies - -**Rule:** Use tagged errors and custom error types to enable type-safe error handling and business-logic-aware recovery strategies. - -**Skill Level:** advanced - -**Use Cases:** error-handling - -### Good Example - -This example demonstrates custom error strategies. - -```typescript -import { Effect, Data, Schedule } from "effect"; - -// Custom domain errors -class NetworkError extends Data.TaggedError("NetworkError")<{ - endpoint: string; - statusCode?: number; - retryable: boolean; -}> {} - -class ValidationError extends Data.TaggedError("ValidationError")<{ - field: string; - reason: string; -}> {} - -class AuthenticationError extends Data.TaggedError("AuthenticationError")<{ - reason: "invalid-token" | "expired-token" | "missing-token"; -}> {} - -class PermissionError extends Data.TaggedError("PermissionError")<{ - resource: string; - action: string; -}> {} - -class RateLimitError extends Data.TaggedError("RateLimitError")<{ - retryAfter: number; // milliseconds -}> {} - -class NotFoundError extends Data.TaggedError("NotFoundError")<{ - resource: string; - id: string; -}> {} - -// Recovery strategy selector -const selectRecoveryStrategy = ( - error: Error -): "retry" | "fallback" | "fail" | "user-message" => { - if (error instanceof NetworkError && error.retryable) { - return "retry"; - } - - if (error instanceof RateLimitError) { - return "retry"; // With backoff - } - - if (error instanceof ValidationError) { - return "user-message"; // User can fix - } - - if (error instanceof NotFoundError) { - return "fallback"; // Use empty result - } - - if ( - error instanceof AuthenticationError && - error.reason === "expired-token" - ) { - return "retry"; // Refresh token - } - - if (error instanceof PermissionError) { - return "fail"; // Don't retry - } - - return "fail"; // Default: don't retry -}; - -const program = Effect.gen(function* () { - console.log( - `\n[CUSTOM ERROR STRATEGIES] Domain-aware error handling\n` - ); - - // Example 1: Type-safe error handling - console.log(`[1] Type-safe error catching:\n`); - - const operation1 = Effect.fail( - new ValidationError({ - field: "email", - reason: "Invalid format", - }) - ); - - const handled1 = operation1.pipe( - Effect.catchTag("ValidationError", (error) => - Effect.gen(function* () { - yield* Effect.log(`[CAUGHT] Validation error`); - yield* Effect.log(` Field: ${error.field}`); - yield* Effect.log(` Reason: ${error.reason}\n`); - - return "validation-failed"; - }) - ) - ); - - yield* handled1; - - // Example 2: Multiple error types with different recovery - console.log(`[2] Different recovery per error type:\n`); - - interface ApiResponse { - status: number; - body?: unknown; - } - - const callApi = (shouldFail: "network" | "validation" | "ratelimit" | "success") => - Effect.gen(function* () { - switch (shouldFail) { - case "network": - yield* Effect.fail( - new NetworkError({ - endpoint: "https://api.example.com/data", - statusCode: 503, - retryable: true, - }) - ); - - case "validation": - yield* Effect.fail( - new ValidationError({ - field: "id", - reason: "Must be numeric", - }) - ); - - case "ratelimit": - yield* Effect.fail( - new RateLimitError({ - retryAfter: 5000, - }) - ); - - case "success": - return { status: 200, body: { id: 123 } }; - } - }); - - // Test each error type - const testCases = ["network", "validation", "ratelimit", "success"] as const; - - for (const testCase of testCases) { - const strategy = yield* callApi(testCase).pipe( - Effect.catchTag("NetworkError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[NETWORK] Retryable: ${error.retryable}, Status: ${error.statusCode}` - ); - - return "will-retry"; - }) - ), - Effect.catchTag("ValidationError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[VALIDATION] ${error.field}: ${error.reason} (no retry)` - ); - - return "user-must-fix"; - }) - ), - Effect.catchTag("RateLimitError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[RATE-LIMIT] Retry after ${error.retryAfter}ms` - ); - - return "retry-with-backoff"; - }) - ), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[SUCCESS] Got response`); - - return "completed"; - }) - ) - ); - - yield* Effect.log(` Strategy: ${strategy}\n`); - } - - // Example 3: Custom retry strategy based on error - console.log(`[3] Error-specific retry strategies:\n`); - - let attemptCount = 0; - - const networkOperation = Effect.gen(function* () { - attemptCount++; - - yield* Effect.log(`[ATTEMPT] ${attemptCount}`); - - if (attemptCount === 1) { - yield* Effect.fail( - new NetworkError({ - endpoint: "api.example.com", - statusCode: 502, - retryable: true, - }) - ); - } - - if (attemptCount === 2) { - yield* Effect.fail( - new RateLimitError({ - retryAfter: 100, - }) - ); - } - - return "success"; - }); - - // Type-safe retry with error classification - let result3: string | null = null; - - for (let i = 0; i < 3; i++) { - result3 = yield* networkOperation.pipe( - Effect.catchTag("NetworkError", (error) => - Effect.gen(function* () { - if (error.retryable && i < 2) { - yield* Effect.log(`[RETRY] Network error is retryable`); - - return null; // Signal to retry - } - - yield* Effect.log(`[FAIL] Network error not retryable`); - - return Effect.fail(error); - }) - ), - Effect.catchTag("RateLimitError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[BACKOFF] Rate limited, waiting ${error.retryAfter}ms` - ); - - yield* Effect.sleep(`${error.retryAfter} millis`); - - return null; // Signal to retry - }) - ), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[ERROR] Unhandled: ${error}`); - - return Effect.fail(error); - }) - ) - ).pipe( - Effect.catchAll(() => Effect.succeed(null)) - ); - - if (result3 !== null) { - break; - } - } - - yield* Effect.log(`\n[RESULT] ${result3}\n`); - - // Example 4: Error-aware business logic - console.log(`[4] Business logic with error handling:\n`); - - interface User { - id: string; - email: string; - } - - const loadUser = (id: string): Effect.Effect => - Effect.gen(function* () { - if (id === "invalid") { - yield* Effect.fail( - new NotFoundError({ - resource: "user", - id, - }) - ); - } - - if (id === "network-error") { - yield* Effect.fail( - new NetworkError({ - endpoint: "/api/users", - retryable: true, - }) - ); - } - - return { id, email: `user-${id}@example.com` }; - }); - - const processUser = (id: string) => - loadUser(id).pipe( - Effect.catchTag("NotFoundError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[BUSINESS] User not found: ${error.id}` - ); - - // Return default/empty user - return { id: "", email: "" }; - }) - ), - Effect.catchTag("NetworkError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[BUSINESS] Network error, will retry from cache` - ); - - return { id, email: "cached@example.com" }; - }) - ) - ); - - yield* processUser("valid-id"); - - yield* processUser("invalid"); - - yield* processUser("network-error"); - - // Example 5: Discriminated union for exhaustiveness - console.log(`\n[5] Exhaustiveness checking (compile-time safety):\n`); - - const classifyError = ( - error: NetworkError | ValidationError | AuthenticationError | PermissionError - ): string => { - switch (error._tag) { - case "NetworkError": - return `network: ${error.statusCode}`; - - case "ValidationError": - return `validation: ${error.field}`; - - case "AuthenticationError": - return `auth: ${error.reason}`; - - case "PermissionError": - return `permission: ${error.action}`; - - // TypeScript ensures all cases covered - default: - const _exhaustive: never = error; - return _exhaustive; - } - }; - - const testError = new ValidationError({ - field: "age", - reason: "Must be >= 18", - }); - - const classification = classifyError(testError); - - yield* Effect.log(`[CLASSIFY] ${classification}`); - - // Example 6: Recovery strategy chains - console.log(`\n[6] Chained recovery strategies:\n`); - - const resilientOperation = Effect.gen(function* () { - yield* Effect.fail( - new RateLimitError({ - retryAfter: 50, - }) - ); - }); - - const withRecovery = resilientOperation.pipe( - Effect.catchTag("RateLimitError", (error) => - Effect.gen(function* () { - yield* Effect.log( - `[STEP 1] Caught rate limit, waiting ${error.retryAfter}ms` - ); - - yield* Effect.sleep(`${error.retryAfter} millis`); - - // Try again - return yield* Effect.succeed("recovered"); - }) - ), - Effect.catchTag("NetworkError", (error) => - Effect.gen(function* () { - if (error.retryable) { - yield* Effect.log(`[STEP 2] Network error, retrying...`); - - return "retry"; - } - - return yield* Effect.fail(error); - }) - ), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[STEP 3] Final fallback`); - - return "fallback"; - }) - ) - ); - - yield* withRecovery; -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Generic errors prevent optimal recovery: - -**Problem 1: One-size-fits-all retry** -- Network timeout (transient, retry with backoff) -- Invalid API key (permanent, don't retry) -- Both treated same = wrong recovery - -**Problem 2: Lost business intent** -- System error: "Connection refused" -- Business meaning: Unclear -- User message: "Something went wrong" (not helpful) - -**Problem 3: Wrong recovery layer** -- Should retry at network layer -- Instead retried at application layer -- Wasted compute, poor user experience - -**Problem 4: Silent failures** -- Multiple error types possible -- Generic catch ignores distinctions -- Bug: handled Error A as if it were Error B -- Data corruption, hard to debug - -Solutions: - -**Tagged errors**: -- `NetworkError`, `ValidationError`, `PermissionError` -- Type system ensures handling -- TypeScript compiler catches missed cases -- Clear intent - -**Recovery strategies**: -- `NetworkError` → Retry with exponential backoff -- `ValidationError` → Return user message, no retry -- `PermissionError` → Log security event, no retry -- `TemporaryError` → Retry with jitter - -**Business semantics**: -- Error type matches domain concept -- Code reads like domain language -- Easier to maintain -- New developers understand quickly - ---- - ---- - -## Execute Asynchronous Effects with Effect.runPromise - -**Rule:** Execute asynchronous effects with Effect.runPromise. - -**Skill Level:** beginner - -**Use Cases:** project-setup--execution - -### Good Example - -```typescript -import { Effect } from "effect"; - -const program = Effect.succeed("Hello, World!").pipe(Effect.delay("1 second")); - -const promise = Effect.runPromise(program); - -const programWithLogging = Effect.gen(function* () { - const result = yield* program; - yield* Effect.log(result); // Logs "Hello, World!" after 1 second. - return result; -}); - -Effect.runPromise(programWithLogging); -``` - -**Explanation:** -`Effect.runPromise` executes your effect and returns a Promise, making it -easy to integrate with existing JavaScript async workflows. - -### Anti-Pattern - -Never call `runPromise` inside another `Effect` composition. Effects are -meant to be composed together _before_ being run once at the end. - -### Explanation - -`Effect.runPromise` is the bridge from the Effect world to the Promise-based -world of Node.js and browsers. If the Effect succeeds, the Promise resolves; -if it fails, the Promise rejects. - ---- - -## Execute Long-Running Apps with Effect.runFork - -**Rule:** Use Effect.runFork to launch a long-running application as a manageable, detached fiber. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This example starts a simple "server" that runs forever. We use `runFork` to launch it and then use the returned `Fiber` to shut it down gracefully after 5 seconds. - -```typescript -import { Effect, Fiber } from "effect"; - -// A server that listens for requests forever -const server = Effect.log("Server received a request.").pipe( - Effect.delay("1 second"), - Effect.forever -); - -Effect.runSync(Effect.log("Starting server...")); - -// Launch the server as a detached, top-level fiber -const appFiber = Effect.runFork(server); - -// In a real app, you would listen for OS signals. -// Here, we simulate a shutdown signal after 5 seconds. -setTimeout(() => { - const shutdownProgram = Effect.gen(function* () { - yield* Effect.log("Shutdown signal received. Interrupting server fiber..."); - // This ensures all cleanup logic within the server effect would run. - yield* Fiber.interrupt(appFiber); - }); - Effect.runPromise(shutdownProgram); -}, 5000); -``` - ---- - -### Anti-Pattern - -Using `runFork` when you immediately need the result of the effect. If you call `runFork` and then immediately call `Fiber.join` on the result, you have simply implemented a more complex and less direct version of `runPromise`. - -```typescript -import { Effect, Fiber } from "effect"; - -const someEffect = Effect.succeed(42); - -// ❌ WRONG: This is just a complicated way to write `Effect.runPromise(someEffect)` -const resultPromise = Effect.runFork(someEffect).pipe( - Fiber.join, - Effect.runPromise -); -``` - -### Explanation - -Unlike `Effect.runPromise`, which waits for the effect to complete, `Effect.runFork` starts the effect and immediately returns a `Fiber`. This is the ideal way to run an application that is meant to run forever, because it gives you a handle to the process. - -The most critical use case for this is enabling graceful shutdown. You can start your application with `runFork`, and then set up listeners for OS signals (like `SIGINT` for Ctrl+C). When a shutdown signal is received, you call `Fiber.interrupt` on the application fiber, which guarantees that all finalizers (like closing database connections) are run before the process exits. - ---- - ---- - -## Execute Synchronous Effects with Effect.runSync - -**Rule:** Execute synchronous effects with Effect.runSync. - -**Skill Level:** beginner - -**Use Cases:** project-setup--execution - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Simple synchronous program -const program1 = Effect.gen(function* () { - const n = 10; - const result = n * 2; - yield* Effect.log(`Simple program result: ${result}`); - return result; -}); - -// Run simple program -Effect.runSync(program1); - -// Program with logging -const program2 = Effect.gen(function* () { - yield* Effect.logInfo("Starting calculation..."); - const n = yield* Effect.sync(() => 10); - yield* Effect.logInfo(`Got number: ${n}`); - const result = yield* Effect.sync(() => n * 2); - yield* Effect.logInfo(`Result: ${result}`); - return result; -}); - -// Run with logging -Effect.runSync(program2); - -// Program with error handling -const program3 = Effect.gen(function* () { - yield* Effect.logInfo("Starting division..."); - const n = yield* Effect.sync(() => 10); - const divisor = yield* Effect.sync(() => 0); - - yield* Effect.logInfo(`Attempting to divide ${n} by ${divisor}...`); - return yield* Effect.try({ - try: () => { - if (divisor === 0) throw new Error("Cannot divide by zero"); - return n / divisor; - }, - catch: (error) => { - if (error instanceof Error) { - return error; - } - return new Error("Unknown error occurred"); - }, - }); -}).pipe( - Effect.catchAll((error) => Effect.logInfo(`Error occurred: ${error.message}`)) -); - -// Run with error handling -Effect.runSync(program3); -``` - -**Explanation:** -Use `runSync` only for Effects that are fully synchronous. If the Effect -contains async code, use `runPromise` instead. - -### Anti-Pattern - -Do not use `runSync` on an Effect that contains asynchronous operations like -`Effect.delay` or `Effect.promise`. This will result in a runtime error. - -### Explanation - -`Effect.runSync` is an optimized runner for Effects that don't involve any -asynchronous operations. If the Effect contains any async operations, -`runSync` will throw an error. - ---- - -## Export Metrics to Prometheus - -**Rule:** Use Effect metrics and expose a /metrics endpoint for Prometheus scraping. - -**Skill Level:** advanced - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, Metric, MetricLabel, Duration } from "effect" -import { HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. Define application metrics -// ============================================ - -// Counter - counts events -const httpRequestsTotal = Metric.counter("http_requests_total", { - description: "Total number of HTTP requests", -}) - -// Counter with labels -const httpRequestsByStatus = Metric.counter("http_requests_by_status", { - description: "HTTP requests by status code", -}) - -// Gauge - current value -const activeConnections = Metric.gauge("active_connections", { - description: "Number of active connections", -}) - -// Histogram - distribution of values -const requestDuration = Metric.histogram("http_request_duration_seconds", { - description: "HTTP request duration in seconds", - boundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], -}) - -// Summary - percentiles -const responseSizeBytes = Metric.summary("http_response_size_bytes", { - description: "HTTP response size in bytes", - maxAge: Duration.minutes(5), - maxSize: 100, - quantiles: [0.5, 0.9, 0.99], -}) - -// ============================================ -// 2. Instrument code with metrics -// ============================================ - -const handleRequest = (path: string, status: number) => - Effect.gen(function* () { - const startTime = Date.now() - - // Increment request counter - yield* Metric.increment(httpRequestsTotal) - - // Increment with labels - yield* Metric.increment( - httpRequestsByStatus.pipe( - Metric.tagged("status", String(status)), - Metric.tagged("path", path) - ) - ) - - // Track active connections - yield* Metric.increment(activeConnections) - - // Simulate work - yield* Effect.sleep("100 millis") - - // Record duration - const duration = (Date.now() - startTime) / 1000 - yield* Metric.update(requestDuration, duration) - - // Record response size - yield* Metric.update(responseSizeBytes, 1024) - - // Decrement active connections - yield* Metric.decrement(activeConnections) - }) - -// ============================================ -// 3. Prometheus text format exporter -// ============================================ - -interface MetricSnapshot { - name: string - type: "counter" | "gauge" | "histogram" | "summary" - help: string - values: Array<{ - labels: Record - value: number - }> - // For histograms - buckets?: Array<{ - le: number - count: number - labels?: Record - }> - sum?: number - count?: number -} - -const formatPrometheusMetrics = (metrics: MetricSnapshot[]): string => { - const lines: string[] = [] - - for (const metric of metrics) { - // Help line - lines.push(`# HELP ${metric.name} ${metric.help}`) - lines.push(`# TYPE ${metric.name} ${metric.type}`) - - // Values - for (const { labels, value } of metric.values) { - const labelStr = Object.entries(labels) - .map(([k, v]) => `${k}="${v}"`) - .join(",") - - if (labelStr) { - lines.push(`${metric.name}{${labelStr}} ${value}`) - } else { - lines.push(`${metric.name} ${value}`) - } - } - - // Histogram buckets - if (metric.buckets) { - for (const bucket of metric.buckets) { - const labelStr = Object.entries(bucket.labels || {}) - .map(([k, v]) => `${k}="${v}"`) - .concat([`le="${bucket.le}"`]) - .join(",") - lines.push(`${metric.name}_bucket{${labelStr}} ${bucket.count}`) - } - lines.push(`${metric.name}_sum ${metric.sum}`) - lines.push(`${metric.name}_count ${metric.count}`) - } - - lines.push("") - } - - return lines.join("\n") -} - -// ============================================ -// 4. /metrics endpoint handler -// ============================================ - -const metricsHandler = Effect.gen(function* () { - // In real implementation, read from Effect's MetricRegistry - const metrics: MetricSnapshot[] = [ - { - name: "http_requests_total", - type: "counter", - help: "Total number of HTTP requests", - values: [{ labels: {}, value: 1234 }], - }, - { - name: "http_requests_by_status", - type: "counter", - help: "HTTP requests by status code", - values: [ - { labels: { status: "200", path: "/api/users" }, value: 1000 }, - { labels: { status: "404", path: "/api/users" }, value: 50 }, - { labels: { status: "500", path: "/api/users" }, value: 10 }, - ], - }, - { - name: "active_connections", - type: "gauge", - help: "Number of active connections", - values: [{ labels: {}, value: 42 }], - }, - { - name: "http_request_duration_seconds", - type: "histogram", - help: "HTTP request duration in seconds", - values: [], - buckets: [ - { le: 0.01, count: 100 }, - { le: 0.05, count: 500 }, - { le: 0.1, count: 800 }, - { le: 0.25, count: 950 }, - { le: 0.5, count: 990 }, - { le: 1, count: 999 }, - { le: Infinity, count: 1000 }, - ], - sum: 123.456, - count: 1000, - }, - ] - - const body = formatPrometheusMetrics(metrics) - - return HttpServerResponse.text(body, { - headers: { - "Content-Type": "text/plain; version=0.0.4; charset=utf-8", - }, - }) -}) - -// ============================================ -// 5. Example output -// ============================================ - -/* -# HELP http_requests_total Total number of HTTP requests -# TYPE http_requests_total counter -http_requests_total 1234 - -# HELP http_requests_by_status HTTP requests by status code -# TYPE http_requests_by_status counter -http_requests_by_status{status="200",path="/api/users"} 1000 -http_requests_by_status{status="404",path="/api/users"} 50 -http_requests_by_status{status="500",path="/api/users"} 10 - -# HELP active_connections Number of active connections -# TYPE active_connections gauge -active_connections 42 - -# HELP http_request_duration_seconds HTTP request duration in seconds -# TYPE http_request_duration_seconds histogram -http_request_duration_seconds_bucket{le="0.01"} 100 -http_request_duration_seconds_bucket{le="0.05"} 500 -http_request_duration_seconds_bucket{le="0.1"} 800 -http_request_duration_seconds_bucket{le="+Inf"} 1000 -http_request_duration_seconds_sum 123.456 -http_request_duration_seconds_count 1000 -*/ -``` - -### Explanation - -Prometheus metrics enable: - -1. **Real-time monitoring** - See what's happening now -2. **Historical analysis** - Track trends over time -3. **Alerting** - Get notified of issues -4. **Dashboards** - Visualize system health - ---- - ---- - -## Extract Path Parameters - -**Rule:** Define routes with colon-prefixed parameters (e.g., /users/:id) and access their values within the handler. - -**Skill Level:** beginner - -**Use Cases:** building-apis - -### Good Example - -This example defines a route that captures a `userId`. The handler for this route accesses the parsed parameters and uses the `userId` to construct a personalized greeting. The router automatically makes the parameters available to the handler. - -```typescript -import { Data, Effect } from "effect"; - -// Define tagged error for invalid paths -interface InvalidPathErrorSchema { - readonly _tag: "InvalidPathError"; - readonly path: string; -} - -const makeInvalidPathError = (path: string): InvalidPathErrorSchema => ({ - _tag: "InvalidPathError", - path, -}); - -// Define service interface -interface PathOps { - readonly extractUserId: ( - path: string - ) => Effect.Effect; - readonly greetUser: (userId: string) => Effect.Effect; -} - -// Create service -class PathService extends Effect.Service()("PathService", { - sync: () => ({ - extractUserId: (path: string) => - Effect.gen(function* () { - yield* Effect.logInfo( - `Attempting to extract user ID from path: ${path}` - ); - - const match = path.match(/\/users\/([^/]+)/); - if (!match) { - yield* Effect.logInfo(`No user ID found in path: ${path}`); - return yield* Effect.fail(makeInvalidPathError(path)); - } - - const userId = match[1]; - yield* Effect.logInfo(`Successfully extracted user ID: ${userId}`); - return userId; - }), - - greetUser: (userId: string) => - Effect.gen(function* () { - const greeting = `Hello, user ${userId}!`; - yield* Effect.logInfo(greeting); - return greeting; - }), - }), -}) {} - -// Compose the functions with proper error handling -const processPath = ( - path: string -): Effect.Effect => - Effect.gen(function* () { - const pathService = yield* PathService; - yield* Effect.logInfo(`Processing path: ${path}`); - const userId = yield* pathService.extractUserId(path); - return yield* pathService.greetUser(userId); - }); - -// Run examples with proper error handling -const program = Effect.gen(function* () { - // Test valid paths - yield* Effect.logInfo("=== Testing valid paths ==="); - const result1 = yield* processPath("/users/123"); - yield* Effect.logInfo(`Result 1: ${result1}`); - - const result2 = yield* processPath("/users/abc"); - yield* Effect.logInfo(`Result 2: ${result2}`); - - // Test invalid path - yield* Effect.logInfo("\n=== Testing invalid path ==="); - const result3 = yield* processPath("/invalid/path").pipe( - Effect.catchTag("InvalidPathError", (error) => - Effect.succeed(`Error: Invalid path ${error.path}`) - ) - ); - yield* Effect.logInfo(result3); -}); - -Effect.runPromise(Effect.provide(program, PathService.Default)); -``` - -### Anti-Pattern - -The anti-pattern is to manually parse the URL string inside the handler. This approach is brittle, imperative, and mixes concerns. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -// This route matches any sub-path of /users/, forcing manual parsing. -const app = Http.router.get( - "/users/*", // Using a wildcard - Http.request.ServerRequest.pipe( - Effect.flatMap((req) => { - // Manually split the URL to find the ID. - const parts = req.url.split("/"); // e.g., ['', 'users', '123'] - if (parts.length === 3 && parts[2]) { - const userId = parts[2]; - return Http.response.text(`Hello, user ${userId}!`); - } - // Manual handling for missing ID. - return Http.response.empty({ status: 404 }); - }) - ) -); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This manual method is highly discouraged. It's fragile—a change in the base path or an extra slash could break the logic (`parts[2]`). It's also not declarative; the intent is hidden inside imperative code. The router's built-in parameter handling is safer, clearer, and the correct approach. - -### Explanation - -APIs often need to operate on specific resources identified by a unique key in the URL, such as `/products/123` or `/orders/abc`. The `Http.router` provides a clean, declarative way to handle these dynamic paths without resorting to manual string parsing. - -By defining parameters directly in the path string, you gain several benefits: - -1. **Declarative**: The route's structure is immediately obvious from its definition. The code clearly states, "this route expects a dynamic segment here." -2. **Safe and Robust**: The router handles the logic of extracting the parameter. This is less error-prone and more robust than manually splitting or using regular expressions on the URL string. -3. **Clean Handler Logic**: The business logic inside your handler is separated from the concern of URL parsing. The handler simply receives the parameters it needs to do its job. -4. **Composability**: This pattern composes perfectly with the rest of the `Http` module, allowing you to build complex and well-structured APIs. - ---- - ---- - -## Fan Out to Multiple Consumers - -**Rule:** Use broadcast or partition to send stream data to multiple consumers. - -**Skill Level:** advanced - -**Use Cases:** building-data-pipelines - -### Good Example - -```typescript -import { Effect, Stream, Queue, Fiber, Chunk } from "effect" - -// ============================================ -// 1. Broadcast to all consumers -// ============================================ - -const broadcastExample = Effect.scoped( - Effect.gen(function* () { - const source = Stream.fromIterable([1, 2, 3, 4, 5]) - - // Broadcast to 3 consumers - each gets all items - const [stream1, stream2, stream3] = yield* Stream.broadcast(source, 3) - - // Consumer 1: Log items - const consumer1 = stream1.pipe( - Stream.tap((n) => Effect.log(`Consumer 1: ${n}`)), - Stream.runDrain - ) - - // Consumer 2: Sum items - const consumer2 = stream2.pipe( - Stream.runFold(0, (acc, n) => acc + n), - Effect.tap((sum) => Effect.log(`Consumer 2 sum: ${sum}`)) - ) - - // Consumer 3: Collect to array - const consumer3 = stream3.pipe( - Stream.runCollect, - Effect.tap((items) => Effect.log(`Consumer 3 collected: ${Chunk.toReadonlyArray(items)}`)) - ) - - // Run all consumers in parallel - yield* Effect.all([consumer1, consumer2, consumer3], { concurrency: 3 }) - }) -) - -// ============================================ -// 2. Partition by predicate -// ============================================ - -const partitionExample = Effect.gen(function* () { - const numbers = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - - // Partition into even and odd - const [evens, odds] = yield* Stream.partition( - numbers, - (n) => n % 2 === 0 - ) - - const processEvens = evens.pipe( - Stream.tap((n) => Effect.log(`Even: ${n}`)), - Stream.runDrain - ) - - const processOdds = odds.pipe( - Stream.tap((n) => Effect.log(`Odd: ${n}`)), - Stream.runDrain - ) - - yield* Effect.all([processEvens, processOdds], { concurrency: 2 }) -}) - -// ============================================ -// 3. Partition into multiple buckets -// ============================================ - -interface Event { - type: "click" | "scroll" | "submit" - data: unknown -} - -const multiPartitionExample = Effect.gen(function* () { - const events: Event[] = [ - { type: "click", data: { x: 100 } }, - { type: "scroll", data: { y: 200 } }, - { type: "submit", data: { form: "login" } }, - { type: "click", data: { x: 150 } }, - { type: "scroll", data: { y: 300 } }, - ] - - const source = Stream.fromIterable(events) - - // Group by type using groupByKey - const grouped = source.pipe( - Stream.groupByKey((event) => event.type, { - bufferSize: 16, - }) - ) - - // Process each group - yield* grouped.pipe( - Stream.flatMap(([key, stream]) => - stream.pipe( - Stream.tap((event) => Effect.log(`[${key}] Processing: ${JSON.stringify(event.data)}`)), - Stream.runDrain, - Stream.fromEffect - ) - ), - Stream.runDrain - ) -}) - -// ============================================ -// 4. Fan-out with queues (manual control) -// ============================================ - -const queueFanOut = Effect.gen(function* () { - const source = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - - // Create queues for each consumer - const queue1 = yield* Queue.unbounded() - const queue2 = yield* Queue.unbounded() - const queue3 = yield* Queue.unbounded() - - // Distribute items round-robin - const distributor = source.pipe( - Stream.zipWithIndex, - Stream.tap(([item, index]) => { - const queue = index % 3 === 0 ? queue1 : index % 3 === 1 ? queue2 : queue3 - return Queue.offer(queue, item) - }), - Stream.runDrain, - Effect.tap(() => Effect.all([ - Queue.shutdown(queue1), - Queue.shutdown(queue2), - Queue.shutdown(queue3), - ])) - ) - - // Consumers - const makeConsumer = (name: string, queue: Queue.Queue) => - Stream.fromQueue(queue).pipe( - Stream.tap((n) => Effect.log(`${name}: ${n}`)), - Stream.runDrain - ) - - yield* Effect.all([ - distributor, - makeConsumer("Worker 1", queue1), - makeConsumer("Worker 2", queue2), - makeConsumer("Worker 3", queue3), - ], { concurrency: 4 }) -}) - -// ============================================ -// 5. Run examples -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Broadcast Example ===") - yield* broadcastExample - - yield* Effect.log("\n=== Partition Example ===") - yield* partitionExample -}) - -Effect.runPromise(program) -``` - -### Explanation - -Fan-out enables parallel processing: - -1. **Throughput** - Multiple consumers process faster -2. **Specialization** - Different consumers handle different data -3. **Redundancy** - Multiple copies for reliability -4. **Decoupling** - Consumers evolve independently - ---- - ---- - -## Filtering Results with filter - -**Rule:** Use filter to declaratively express conditional logic, keeping only values that satisfy a predicate. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Stream, Option, Either } from "effect"; - -// Effect: Only succeed if the value is even, fail otherwise -const effect = Effect.succeed(4).pipe( - Effect.filterOrFail( - (n): n is number => n % 2 === 0, - () => "Number is not even" - ) -); // Effect - -// Option: Only keep the value if it is even -const option = Option.some(4).pipe( - Option.filter((n): n is number => n % 2 === 0) -); // Option - -// Either: Use map and flatMap to filter -const either = Either.right(4).pipe( - Either.flatMap((n) => - n % 2 === 0 ? Either.right(n) : Either.left("Number is not even") - ) -); // Either - -// Stream: Only emit even numbers -const stream = Stream.fromIterable([1, 2, 3, 4]).pipe( - Stream.filter((n): n is number => n % 2 === 0) -); // Stream -``` - -**Explanation:** -`filter` applies a predicate to the value(s) inside the structure. If the predicate fails, the result is a failure (`Effect.fail`, `Either.left`), `Option.none`, or an empty stream. - -### Anti-Pattern - -Using `map` with a conditional that returns `Option` or `Either`, then manually flattening, instead of using `filter`. -This leads to unnecessary complexity and less readable code. - -### Explanation - -`filter` lets you express "only continue if..." logic without resorting to manual checks or imperative branching. -It keeps your code composable and type-safe, and ensures that failures or empty results are handled consistently. - ---- - -## Generate OpenAPI Documentation - -**Rule:** Use Schema definitions to automatically generate OpenAPI documentation for your API. - -**Skill Level:** advanced - -**Use Cases:** building-apis - -### Good Example - -```typescript -import { Effect, Schema } from "effect" -import { - HttpApi, - HttpApiBuilder, - HttpApiEndpoint, - HttpApiGroup, - HttpApiSwagger, - OpenApi, -} from "@effect/platform" - -// ============================================ -// 1. Define schemas for request/response -// ============================================ - -const UserSchema = Schema.Struct({ - id: Schema.String, - email: Schema.String.pipe(Schema.pattern(/@/)), - name: Schema.String, - createdAt: Schema.DateFromString, -}) - -const CreateUserSchema = Schema.Struct({ - email: Schema.String.pipe(Schema.pattern(/@/)), - name: Schema.String, -}) - -const UserListSchema = Schema.Array(UserSchema) - -const ErrorSchema = Schema.Struct({ - error: Schema.String, - code: Schema.String, -}) - -// ============================================ -// 2. Define API endpoints with schemas -// ============================================ - -const usersApi = HttpApiGroup.make("users") - .pipe( - HttpApiGroup.add( - HttpApiEndpoint.get("getUsers", "/users") - .pipe( - HttpApiEndpoint.setSuccess(UserListSchema), - HttpApiEndpoint.addError(ErrorSchema, { status: 500 }) - ) - ), - HttpApiGroup.add( - HttpApiEndpoint.get("getUser", "/users/:id") - .pipe( - HttpApiEndpoint.setPath(Schema.Struct({ - id: Schema.String, - })), - HttpApiEndpoint.setSuccess(UserSchema), - HttpApiEndpoint.addError(ErrorSchema, { status: 404 }), - HttpApiEndpoint.addError(ErrorSchema, { status: 500 }) - ) - ), - HttpApiGroup.add( - HttpApiEndpoint.post("createUser", "/users") - .pipe( - HttpApiEndpoint.setPayload(CreateUserSchema), - HttpApiEndpoint.setSuccess(UserSchema, { status: 201 }), - HttpApiEndpoint.addError(ErrorSchema, { status: 400 }), - HttpApiEndpoint.addError(ErrorSchema, { status: 500 }) - ) - ), - HttpApiGroup.add( - HttpApiEndpoint.del("deleteUser", "/users/:id") - .pipe( - HttpApiEndpoint.setPath(Schema.Struct({ - id: Schema.String, - })), - HttpApiEndpoint.setSuccess(Schema.Void, { status: 204 }), - HttpApiEndpoint.addError(ErrorSchema, { status: 404 }), - HttpApiEndpoint.addError(ErrorSchema, { status: 500 }) - ) - ) - ) - -// ============================================ -// 3. Create the API definition -// ============================================ - -const api = HttpApi.make("My API") - .pipe( - HttpApi.addGroup(usersApi), - OpenApi.annotate({ - title: "My Effect API", - version: "1.0.0", - description: "A sample API built with Effect", - }) - ) - -// ============================================ -// 4. Implement the handlers -// ============================================ - -const usersHandlers = HttpApiBuilder.group(api, "users", (handlers) => - handlers - .pipe( - HttpApiBuilder.handle("getUsers", () => - Effect.succeed([ - { - id: "1", - email: "alice@example.com", - name: "Alice", - createdAt: new Date(), - }, - ]) - ), - HttpApiBuilder.handle("getUser", ({ path }) => - Effect.gen(function* () { - if (path.id === "not-found") { - return yield* Effect.fail({ error: "User not found", code: "NOT_FOUND" }) - } - return { - id: path.id, - email: "user@example.com", - name: "User", - createdAt: new Date(), - } - }) - ), - HttpApiBuilder.handle("createUser", ({ payload }) => - Effect.succeed({ - id: crypto.randomUUID(), - email: payload.email, - name: payload.name, - createdAt: new Date(), - }) - ), - HttpApiBuilder.handle("deleteUser", ({ path }) => - Effect.gen(function* () { - if (path.id === "not-found") { - return yield* Effect.fail({ error: "User not found", code: "NOT_FOUND" }) - } - yield* Effect.log(`Deleted user ${path.id}`) - }) - ) - ) -) - -// ============================================ -// 5. Build the server with Swagger UI -// ============================================ - -const MyApiLive = HttpApiBuilder.api(api).pipe( - Layer.provide(usersHandlers) -) - -const ServerLive = HttpApiBuilder.serve().pipe( - // Add Swagger UI at /docs - Layer.provide(HttpApiSwagger.layer({ path: "/docs" })), - Layer.provide(MyApiLive), - Layer.provide(NodeHttpServer.layer({ port: 3000 })) -) - -// ============================================ -// 6. Export OpenAPI spec as JSON -// ============================================ - -const openApiSpec = OpenApi.fromApi(api) - -// Save to file for external tools -import { NodeFileSystem } from "@effect/platform-node" - -const saveSpec = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - yield* fs.writeFileString( - "openapi.json", - JSON.stringify(openApiSpec, null, 2) - ) - yield* Effect.log("OpenAPI spec saved to openapi.json") -}) -``` - -### Explanation - -OpenAPI documentation provides: - -1. **Discovery** - Clients know what endpoints exist -2. **Contracts** - Clear request/response shapes -3. **Testing** - Swagger UI for manual testing -4. **Code generation** - Generate client SDKs -5. **Validation** - Schema-first development - ---- - ---- - -## Handle a GET Request - -**Rule:** Use Http.router.get to associate a URL path with a specific response Effect. - -**Skill Level:** beginner - -**Use Cases:** building-apis - -### Good Example - -This example defines two separate GET routes, one for the root path (`/`) and one for `/hello`. We create an empty router and add each route to it. The resulting `app` is then served. The router automatically handles sending a `404 Not Found` response for any path that doesn't match. - -```typescript -import { Data, Effect } from "effect"; - -// Define response types -interface RouteResponse { - readonly status: number; - readonly body: string; -} - -// Define error types -class RouteNotFoundError extends Data.TaggedError("RouteNotFoundError")<{ - readonly path: string; -}> {} - -class RouteHandlerError extends Data.TaggedError("RouteHandlerError")<{ - readonly path: string; - readonly error: string; -}> {} - -// Define route service -class RouteService extends Effect.Service()("RouteService", { - sync: () => { - // Create instance methods - const handleRoute = ( - path: string - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`Processing request for path: ${path}`); - - try { - switch (path) { - case "/": - const home = "Welcome to the home page!"; - yield* Effect.logInfo(`Serving home page`); - return { status: 200, body: home }; - - case "/hello": - const hello = "Hello, Effect!"; - yield* Effect.logInfo(`Serving hello page`); - return { status: 200, body: hello }; - - default: - yield* Effect.logWarning(`Route not found: ${path}`); - return yield* Effect.fail(new RouteNotFoundError({ path })); - } - } catch (e) { - const error = e instanceof Error ? e.message : String(e); - yield* Effect.logError(`Error handling route ${path}: ${error}`); - return yield* Effect.fail(new RouteHandlerError({ path, error })); - } - }); - - // Return service implementation - return { - handleRoute, - // Simulate GET request - simulateGet: ( - path: string - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`GET ${path}`); - const response = yield* handleRoute(path); - yield* Effect.logInfo(`Response: ${JSON.stringify(response)}`); - return response; - }), - }; - }, -}) {} - -// Create program with proper error handling -const program = Effect.gen(function* () { - const router = yield* RouteService; - - yield* Effect.logInfo("=== Starting Route Tests ==="); - - // Test different routes - for (const path of ["/", "/hello", "/other", "/error"]) { - yield* Effect.logInfo(`\n--- Testing ${path} ---`); - - const result = yield* router.simulateGet(path).pipe( - Effect.catchTags({ - RouteNotFoundError: (error) => - Effect.gen(function* () { - const response = { status: 404, body: `Not Found: ${error.path}` }; - yield* Effect.logWarning(`${response.status} ${response.body}`); - return response; - }), - RouteHandlerError: (error) => - Effect.gen(function* () { - const response = { - status: 500, - body: `Internal Error: ${error.error}`, - }; - yield* Effect.logError(`${response.status} ${response.body}`); - return response; - }), - }) - ); - - yield* Effect.logInfo(`Final Response: ${JSON.stringify(result)}`); - } - - yield* Effect.logInfo("\n=== Route Tests Complete ==="); -}); - -// Run the program -Effect.runPromise(Effect.provide(program, RouteService.Default)); -``` - -### Anti-Pattern - -The anti-pattern is to create a single, monolithic handler that uses conditional logic to inspect the request URL. This imperative approach is difficult to maintain and scale. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -// A single app that manually checks the URL -const app = Http.request.ServerRequest.pipe( - Effect.flatMap((req) => { - if (req.url === "/") { - return Effect.succeed(Http.response.text("Welcome to the home page!")); - } else if (req.url === "/hello") { - return Effect.succeed(Http.response.text("Hello, Effect!")); - } else { - return Effect.succeed(Http.response.empty({ status: 404 })); - } - }) -); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This manual routing logic is verbose, error-prone (a typo in a string breaks the route), and mixes the "what" (the response) with the "where" (the routing). It doesn't scale to handle different HTTP methods, path parameters, or middleware gracefully. The `Http.router` is designed to solve all of these problems elegantly. - -### Explanation - -A real application needs to respond differently to different URLs. The `Http.router` provides a declarative, type-safe, and composable way to manage this routing logic. Instead of a single handler with complex conditional logic, you define many small, focused handlers and assign them to specific paths and HTTP methods. - -This approach has several advantages: - -1. **Declarative and Readable**: Your code clearly expresses the mapping between a URL path and its behavior, making the application's structure easy to understand. -2. **Composability**: Routers are just values that can be created, combined, and passed around. This makes it easy to organize routes into logical groups (e.g., a `userRoutes` router and a `productRoutes` router) and merge them. -3. **Type Safety**: The router ensures that the handler for a route is only ever called for a matching request, simplifying the logic within the handler itself. -4. **Integration**: Each route handler is an `Effect`, meaning it has full access to dependency injection, structured concurrency, and integrated error handling, just like any other part of an Effect application. - ---- - ---- - -## Handle API Errors - -**Rule:** Model application errors as typed classes and use Http.server.serveOptions to map them to specific HTTP responses. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good 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 - ) -); -``` - -### Anti-Pattern - -The anti-pattern is to handle HTTP-specific error logic inside each route handler using functions like `Effect.catchTag`. - -```typescript -import { Effect, Data } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{ - id: string; -}> {} -// ... same getUser function and error classes - -const userRoute = Http.router.get( - "/users/:userId", - Effect.flatMap(Http.request.ServerRequest, (req) => - getUser(req.params.userId) - ).pipe( - Effect.map(Http.response.json), - // Manually catching errors inside the route logic - Effect.catchTag("UserNotFoundError", (e) => - Http.response.text(`User ${e.id} not found`, { status: 404 }) - ), - Effect.catchTag("InvalidIdError", (e) => - Http.response.text(`ID ${e.id} is not a valid format`, { status: 400 }) - ) - ) -); - -const app = Http.router.empty.pipe(Http.router.addRoute(userRoute)); - -// No centralized error handling -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This approach is problematic because it pollutes the business logic of the route handler with details about HTTP status codes. It's also highly repetitive; if ten different routes could produce a `UserNotFoundError`, you would need to copy this `catchTag` logic into all ten of them, making the API difficult to maintain. - -### Explanation - -By default, any unhandled failure in an Effect route handler results in a generic `500 Internal Server Error`. This is a safe default, but it's not helpful for API clients who need to know _why_ their request failed. Was it a client-side error (like a non-existent resource, `404`) or a true server-side problem (`500`)? - -Centralizing error handling at the server level provides a clean separation of concerns: - -1. **Domain-Focused Logic**: Your business logic can fail with specific, descriptive errors (e.g., `UserNotFoundError`) without needing any knowledge of HTTP status codes. -2. **Centralized Mapping**: You define the mapping from application errors to HTTP responses in a single location. This makes your API's error handling consistent and easy to maintain. If you need to change how an error is reported, you only change it in one place. -3. **Type Safety**: Using `Data.TaggedClass` for your errors allows you to use `Match` to exhaustively handle all known error cases, preventing you from forgetting to map a specific error type. -4. **Clear Client Communication**: It produces a predictable and useful API, allowing clients to programmatically react to different failure scenarios. - ---- - ---- - -## Handle Errors with catchTag, catchTags, and catchAll - -**Rule:** Handle errors with catchTag, catchTags, and catchAll. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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. - -### Anti-Pattern - -Using `try/catch` blocks inside your Effect compositions. It breaks the -declarative flow and bypasses Effect's powerful, type-safe error channels. - -### Explanation - -Effect's structured error handling allows you to build resilient applications. -By using tagged errors and `catchTag`, you can handle different failure -scenarios with different logic in a type-safe way. - ---- - -## Handle Flaky Operations with Retries and Timeouts - -**Rule:** Use Effect.retry and Effect.timeout to build resilience against slow or intermittently failing effects. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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)); -``` - ---- - -### Anti-Pattern - -Writing manual retry and timeout logic. This is verbose, complex, and easy to get wrong. It clutters your business logic with concerns that Effect can handle declaratively. - -```typescript -// ❌ WRONG: Manual, complex, and error-prone logic. -async function manualRetryAndTimeout() { - for (let i = 0; i < 3; i++) { - try { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 2000); - - const response = await fetch("...", { signal: controller.signal }); - clearTimeout(timeoutId); - - return await response.json(); - } catch (error) { - if (i === 2) throw error; // Last attempt, re-throw - await new Promise((res) => setTimeout(res, 100 * 2 ** i)); // Manual backoff - } - } -} -``` - -### Explanation - -In distributed systems, failure is normal. APIs can fail intermittently, and network latency can spike. Hard-coding your application to try an operation only once makes it brittle. - -- **Retries:** The `Effect.retry` operator, combined with a `Schedule` policy, provides a powerful, declarative way to handle transient failures. Instead of writing complex `try/catch` loops, you can simply define a policy like "retry 3 times, with an exponential backoff delay between attempts." - -- **Timeouts:** An operation might not fail, but instead hang indefinitely. `Effect.timeout` prevents this by racing your effect against a timer. If your effect doesn't complete within the specified duration, it is automatically interrupted, preventing your application from getting stuck. - -Combining these two patterns is a best practice for any interaction with an external service. - ---- - ---- - -## Handle Missing Values with Option - -**Rule:** Use Option instead of null/undefined to make missing values explicit and type-safe. - -**Skill Level:** beginner - -**Use Cases:** domain-modeling - -### Good Example - -```typescript -import { Option, Effect } from "effect" - -// ============================================ -// 1. Creating Options -// ============================================ - -// Some - a value is present -const hasValue = Option.some(42) - -// None - no value -const noValue = Option.none() - -// From nullable - null/undefined becomes None -const fromNull = Option.fromNullable(null) // None -const fromValue = Option.fromNullable("hello") // Some("hello") - -// ============================================ -// 2. Checking and extracting values -// ============================================ - -const maybeUser = Option.some({ name: "Alice", age: 30 }) - -// Check if value exists -if (Option.isSome(maybeUser)) { - console.log(`User: ${maybeUser.value.name}`) -} - -// Get with default -const name = Option.getOrElse( - Option.map(maybeUser, u => u.name), - () => "Anonymous" -) - -// ============================================ -// 3. Transforming Options -// ============================================ - -const maybeNumber = Option.some(5) - -// Map - transform the value if present -const doubled = Option.map(maybeNumber, n => n * 2) // Some(10) - -// FlatMap - chain operations that return Option -const safeDivide = (a: number, b: number): Option.Option => - b === 0 ? Option.none() : Option.some(a / b) - -const result = Option.flatMap(maybeNumber, n => safeDivide(10, n)) // Some(2) - -// ============================================ -// 4. Domain modeling example -// ============================================ - -interface User { - readonly id: string - readonly name: string - readonly email: Option.Option // Email is optional - readonly phone: Option.Option // Phone is optional -} - -const createUser = (name: string): User => ({ - id: crypto.randomUUID(), - name, - email: Option.none(), - phone: Option.none(), -}) - -const addEmail = (user: User, email: string): User => ({ - ...user, - email: Option.some(email), -}) - -const getContactInfo = (user: User): string => { - const email = Option.getOrElse(user.email, () => "no email") - const phone = Option.getOrElse(user.phone, () => "no phone") - return `${user.name}: ${email}, ${phone}` -} - -// ============================================ -// 5. Use in Effects -// ============================================ - -const findUser = (id: string): Effect.Effect> => - Effect.succeed( - id === "123" - ? Option.some({ id, name: "Alice", email: Option.none(), phone: Option.none() }) - : Option.none() - ) - -const program = Effect.gen(function* () { - const maybeUser = yield* findUser("123") - - if (Option.isSome(maybeUser)) { - yield* Effect.log(`Found: ${maybeUser.value.name}`) - } else { - yield* Effect.log("User not found") - } -}) - -Effect.runPromise(program) -``` - -### Explanation - -`null` and `undefined` cause bugs because: - -1. **Silent failures** - Accessing `.property` on null crashes at runtime -2. **Unclear intent** - Is null "not found" or "error"? -3. **Forgotten checks** - Easy to forget `if (x !== null)` - -Option fixes this by making absence explicit and type-checked. - ---- - ---- - -## Handle Rate Limiting Responses - -**Rule:** Detect 429 responses and automatically retry after the Retry-After period. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Schedule, Duration, Data, Ref } from "effect" -import { HttpClient, HttpClientResponse } from "@effect/platform" - -// ============================================ -// 1. Rate limit error type -// ============================================ - -class RateLimitedError extends Data.TaggedError("RateLimitedError")<{ - readonly retryAfter: number - readonly limit: number | undefined - readonly remaining: number | undefined - readonly reset: number | undefined -}> {} - -// ============================================ -// 2. Parse rate limit headers -// ============================================ - -interface RateLimitInfo { - readonly retryAfter: number - readonly limit?: number - readonly remaining?: number - readonly reset?: number -} - -const parseRateLimitHeaders = (headers: Record): RateLimitInfo => { - // Parse Retry-After (seconds or date) - const retryAfterHeader = headers["retry-after"] - let retryAfter = 60 // Default 60 seconds - - if (retryAfterHeader) { - const parsed = parseInt(retryAfterHeader, 10) - if (!isNaN(parsed)) { - retryAfter = parsed - } else { - // Try parsing as date - const date = Date.parse(retryAfterHeader) - if (!isNaN(date)) { - retryAfter = Math.max(0, Math.ceil((date - Date.now()) / 1000)) - } - } - } - - return { - retryAfter, - limit: headers["x-ratelimit-limit"] ? parseInt(headers["x-ratelimit-limit"], 10) : undefined, - remaining: headers["x-ratelimit-remaining"] ? parseInt(headers["x-ratelimit-remaining"], 10) : undefined, - reset: headers["x-ratelimit-reset"] ? parseInt(headers["x-ratelimit-reset"], 10) : undefined, - } -} - -// ============================================ -// 3. HTTP client with rate limit handling -// ============================================ - -const makeRateLimitAwareClient = Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - - return { - get: (url: string) => - Effect.gen(function* () { - const response = yield* httpClient.get(url) - - if (response.status === 429) { - const rateLimitInfo = parseRateLimitHeaders(response.headers) - - yield* Effect.log( - `Rate limited. Retry after ${rateLimitInfo.retryAfter}s` - ) - - return yield* Effect.fail(new RateLimitedError({ - retryAfter: rateLimitInfo.retryAfter, - limit: rateLimitInfo.limit, - remaining: rateLimitInfo.remaining, - reset: rateLimitInfo.reset, - })) - } - - return yield* HttpClientResponse.json(response) as Effect.Effect - }).pipe( - Effect.retry({ - schedule: Schedule.recurWhile( - (e) => e._tag === "RateLimitedError" - ).pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.delayed((_, error) => - Duration.seconds(error.retryAfter + 1) // Add 1s buffer - ) - ), - while: (error) => error._tag === "RateLimitedError", - }) - ), - } -}) - -// ============================================ -// 4. Proactive rate limiting (client-side) -// ============================================ - -interface RateLimiter { - readonly acquire: () => Effect.Effect - readonly release: () => Effect.Effect -} - -const makeClientRateLimiter = (requestsPerSecond: number) => - Effect.gen(function* () { - const tokens = yield* Ref.make(requestsPerSecond) - const interval = 1000 / requestsPerSecond - - // Refill tokens periodically - yield* Effect.fork( - Effect.forever( - Effect.gen(function* () { - yield* Effect.sleep(Duration.millis(interval)) - yield* Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond)) - }) - ) - ) - - const limiter: RateLimiter = { - acquire: () => - Effect.gen(function* () { - let acquired = false - while (!acquired) { - const current = yield* Ref.get(tokens) - if (current > 0) { - yield* Ref.update(tokens, (n) => n - 1) - acquired = true - } else { - yield* Effect.sleep(Duration.millis(interval)) - } - } - }), - - release: () => Ref.update(tokens, (n) => Math.min(n + 1, requestsPerSecond)), - } - - return limiter - }) - -// ============================================ -// 5. Combined client -// ============================================ - -const makeRobustHttpClient = (requestsPerSecond: number) => - Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - const rateLimiter = yield* makeClientRateLimiter(requestsPerSecond) - - return { - get: (url: string) => - Effect.gen(function* () { - // Wait for rate limiter token - yield* rateLimiter.acquire() - - const response = yield* httpClient.get(url) - - if (response.status === 429) { - const info = parseRateLimitHeaders(response.headers) - yield* Effect.log(`Server rate limit hit, waiting ${info.retryAfter}s`) - yield* Effect.sleep(Duration.seconds(info.retryAfter)) - return yield* Effect.fail(new Error("Rate limited")) - } - - return yield* HttpClientResponse.json(response) as Effect.Effect - }).pipe( - Effect.retry( - Schedule.exponential("1 second").pipe( - Schedule.intersect(Schedule.recurs(3)) - ) - ) - ), - } - }) - -// ============================================ -// 6. Batch requests to stay under limits -// ============================================ - -const batchRequests = ( - urls: string[], - requestsPerSecond: number -) => - Effect.gen(function* () { - const httpClient = yield* HttpClient.HttpClient - const results: T[] = [] - const interval = 1000 / requestsPerSecond - - for (const url of urls) { - const response = yield* httpClient.get(url) - const data = yield* HttpClientResponse.json(response) as Effect.Effect - results.push(data) - - // Wait between requests - if (urls.indexOf(url) < urls.length - 1) { - yield* Effect.sleep(Duration.millis(interval)) - } - } - - return results - }) - -// ============================================ -// 7. Usage -// ============================================ - -const program = Effect.gen(function* () { - const client = yield* makeRateLimitAwareClient - - yield* Effect.log("Making rate-limited request...") - - const data = yield* client.get("https://api.example.com/data").pipe( - Effect.catchTag("RateLimitedError", (error) => - Effect.gen(function* () { - yield* Effect.log(`Gave up after rate limiting. Limit: ${error.limit}`) - return { error: "rate_limited" } - }) - ) - ) - - yield* Effect.log(`Result: ${JSON.stringify(data)}`) -}) -``` - -### Explanation - -Rate limits protect APIs: - -1. **Fair usage** - Share resources among clients -2. **Stability** - Prevent overload -3. **Quotas** - Enforce billing tiers - -Respecting limits prevents bans and ensures reliable access. - ---- - ---- - -## Handle Resource Timeouts - -**Rule:** Always set timeouts on resource acquisition to prevent indefinite waits. - -**Skill Level:** intermediate - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Duration, Scope } from "effect" - -// ============================================ -// 1. Define a resource with slow acquisition -// ============================================ - -interface Connection { - readonly id: string - readonly query: (sql: string) => Effect.Effect -} - -const acquireConnection = Effect.gen(function* () { - yield* Effect.log("Attempting to connect...") - - // Simulate slow connection - yield* Effect.sleep("2 seconds") - - const connection: Connection = { - id: crypto.randomUUID(), - query: (sql) => Effect.succeed({ rows: [] }), - } - - yield* Effect.log(`Connected: ${connection.id}`) - return connection -}) - -const releaseConnection = (conn: Connection) => - Effect.log(`Released: ${conn.id}`) - -// ============================================ -// 2. Timeout on acquisition -// ============================================ - -const acquireWithTimeout = acquireConnection.pipe( - Effect.timeout("1 second"), - Effect.catchTag("TimeoutException", () => - Effect.fail(new Error("Connection timeout - database unreachable")) - ) -) - -// ============================================ -// 3. Timeout on usage -// ============================================ - -const queryWithTimeout = (conn: Connection, sql: string) => - conn.query(sql).pipe( - Effect.timeout("5 seconds"), - Effect.catchTag("TimeoutException", () => - Effect.fail(new Error(`Query timeout: ${sql}`)) - ) - ) - -// ============================================ -// 4. Full resource lifecycle with timeouts -// ============================================ - -const useConnectionWithTimeouts = Effect.acquireRelease( - acquireWithTimeout, - releaseConnection -).pipe( - Effect.flatMap((conn) => - Effect.gen(function* () { - yield* Effect.log("Running queries...") - - // Each query has its own timeout - const result1 = yield* queryWithTimeout(conn, "SELECT 1") - const result2 = yield* queryWithTimeout(conn, "SELECT 2") - - return [result1, result2] - }) - ), - Effect.scoped -) - -// ============================================ -// 5. Timeout on entire operation -// ============================================ - -const entireOperationWithTimeout = useConnectionWithTimeouts.pipe( - Effect.timeout("10 seconds"), - Effect.catchTag("TimeoutException", () => - Effect.fail(new Error("Entire operation timed out")) - ) -) - -// ============================================ -// 6. Run with different scenarios -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Testing timeouts ===") - - const result = yield* entireOperationWithTimeout.pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Failed: ${error.message}`) - return [] - }) - ) - ) - - yield* Effect.log(`Result: ${JSON.stringify(result)}`) -}) - -Effect.runPromise(program) -``` - -### Explanation - -Resources can become unavailable: - -1. **Network partitions** - Can't reach database -2. **Pool exhaustion** - All connections in use -3. **Deadlocks** - Resources held indefinitely -4. **Slow operations** - Query takes too long - -Timeouts provide a safety net. - ---- - ---- - -## Handle Unexpected Errors by Inspecting the Cause - -**Rule:** Handle unexpected errors by inspecting the cause. - -**Skill Level:** advanced - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Cause, Effect, Data, Schedule, Duration } from "effect"; - -// Define domain types -interface DatabaseConfig { - readonly url: string; -} - -interface DatabaseConnection { - readonly success: true; -} - -interface UserData { - readonly id: string; - readonly name: string; -} - -// Define error types -class DatabaseError extends Data.TaggedError("DatabaseError")<{ - readonly operation: string; - readonly details: string; -}> {} - -class ValidationError extends Data.TaggedError("ValidationError")<{ - readonly field: string; - readonly message: string; -}> {} - -// Define database service -class DatabaseService extends Effect.Service()( - "DatabaseService", - { - sync: () => ({ - // Connect to database with proper error handling - connect: ( - config: DatabaseConfig - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`Connecting to database: ${config.url}`); - - if (!config.url) { - const error = new DatabaseError({ - operation: "connect", - details: "Missing URL", - }); - yield* Effect.logError(`Database error: ${JSON.stringify(error)}`); - return yield* Effect.fail(error); - } - - // Simulate unexpected errors - if (config.url === "invalid") { - yield* Effect.logError("Invalid connection string"); - return yield* Effect.sync(() => { - throw new Error("Failed to parse connection string"); - }); - } - - if (config.url === "timeout") { - yield* Effect.logError("Connection timeout"); - return yield* Effect.sync(() => { - throw new Error("Connection timed out"); - }); - } - - yield* Effect.logInfo("Database connection successful"); - return { success: true }; - }), - }), - } -) {} - -// Define user service -class UserService extends Effect.Service()("UserService", { - sync: () => ({ - // Parse user data with validation - parseUser: (input: unknown): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`Parsing user data: ${JSON.stringify(input)}`); - - try { - if (typeof input !== "object" || !input) { - const error = new ValidationError({ - field: "input", - message: "Invalid input type", - }); - yield* Effect.logWarning( - `Validation error: ${JSON.stringify(error)}` - ); - throw error; - } - - const data = input as Record; - - if (typeof data.id !== "string" || typeof data.name !== "string") { - const error = new ValidationError({ - field: "input", - message: "Missing required fields", - }); - yield* Effect.logWarning( - `Validation error: ${JSON.stringify(error)}` - ); - throw error; - } - - const user = { id: data.id, name: data.name }; - yield* Effect.logInfo( - `Successfully parsed user: ${JSON.stringify(user)}` - ); - return user; - } catch (e) { - if (e instanceof ValidationError) { - return yield* Effect.fail(e); - } - yield* Effect.logError( - `Unexpected error: ${e instanceof Error ? e.message : String(e)}` - ); - throw e; - } - }), - }), -}) {} - -// Define test service -class TestService extends Effect.Service()("TestService", { - sync: () => { - // Create instance methods - const printCause = ( - prefix: string, - cause: Cause.Cause - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`\n=== ${prefix} ===`); - - if (Cause.isDie(cause)) { - const defect = Cause.failureOption(cause); - if (defect._tag === "Some") { - const error = defect.value as Error; - yield* Effect.logError("Defect (unexpected error)"); - yield* Effect.logError(`Message: ${error.message}`); - yield* Effect.logError( - `Stack: ${error.stack?.split("\n")[1]?.trim() ?? "N/A"}` - ); - } - } else if (Cause.isFailure(cause)) { - const error = Cause.failureOption(cause); - yield* Effect.logWarning("Expected failure"); - yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`); - } - - // Don't return an Effect inside Effect.gen, just return the value directly - return void 0; - }); - - const runScenario = ( - name: string, - program: Effect.Effect - ): Effect.Effect => - Effect.gen(function* () { - yield* Effect.logInfo(`\n=== Testing: ${name} ===`); - - type TestError = { - readonly _tag: "error"; - readonly cause: Cause.Cause; - }; - - const result = yield* Effect.catchAllCause(program, (cause) => - Effect.succeed({ _tag: "error" as const, cause } as TestError) - ); - - if ("cause" in result) { - yield* printCause("Error details", result.cause); - } else { - yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`); - } - - // Don't return an Effect inside Effect.gen, just return the value directly - return void 0; - }); - - // Return bound methods - return { - printCause, - runScenario, - }; - }, -}) {} - -// Create program with proper error handling -const program = Effect.gen(function* () { - const db = yield* DatabaseService; - const users = yield* UserService; - const test = yield* TestService; - - yield* Effect.logInfo("=== Starting Error Handling Tests ==="); - - // Test expected database errors - yield* test.runScenario( - "Expected database error", - Effect.gen(function* () { - const result = yield* Effect.retry( - db.connect({ url: "" }), - Schedule.exponential(100) - ).pipe( - Effect.timeout(Duration.seconds(5)), - Effect.catchAll(() => Effect.fail("Connection timeout")) - ); - return result; - }) - ); - - // Test unexpected connection errors - yield* test.runScenario( - "Unexpected connection error", - Effect.gen(function* () { - const result = yield* Effect.retry( - db.connect({ url: "invalid" }), - Schedule.recurs(3) - ).pipe( - Effect.catchAllCause((cause) => - Effect.gen(function* () { - yield* Effect.logError("Failed after 3 retries"); - yield* Effect.logError(Cause.pretty(cause)); - return yield* Effect.fail("Max retries exceeded"); - }) - ) - ); - return result; - }) - ); - - // Test user validation with recovery - yield* test.runScenario( - "Valid user data", - Effect.gen(function* () { - const result = yield* users - .parseUser({ id: "1", name: "John" }) - .pipe( - Effect.orElse(() => - Effect.succeed({ id: "default", name: "Default User" }) - ) - ); - return result; - }) - ); - - // Test concurrent error handling with timeout - yield* test.runScenario( - "Concurrent operations", - Effect.gen(function* () { - const results = yield* Effect.all( - [ - db.connect({ url: "" }).pipe( - Effect.timeout(Duration.seconds(1)), - Effect.catchAll(() => Effect.succeed({ success: true })) - ), - users.parseUser({ id: "invalid" }).pipe( - Effect.timeout(Duration.seconds(1)), - Effect.catchAll(() => - Effect.succeed({ id: "timeout", name: "Timeout" }) - ) - ), - ], - { concurrency: 2 } - ); - return results; - }) - ); - - yield* Effect.logInfo("\n=== Error Handling Tests Complete ==="); - - // Don't return an Effect inside Effect.gen, just return the value directly - return void 0; -}); - -// Run the program with all services -Effect.runPromise( - Effect.provide( - Effect.provide( - Effect.provide(program, TestService.Default), - DatabaseService.Default - ), - UserService.Default - ) -); -``` - -**Explanation:** -By inspecting the `Cause`, you can distinguish between expected and unexpected -failures, logging or escalating as appropriate. - -### Anti-Pattern - -Using a simple `Effect.catchAll` can dangerously conflate expected errors and -unexpected defects, masking critical bugs as recoverable errors. - -### Explanation - -The `Cause` object explains _why_ an effect failed. A `Fail` is an expected -error (e.g., `ValidationError`). A `Die` is an unexpected defect (e.g., a -thrown exception). They should be handled differently. - ---- - -## Handle Unexpected Errors by Inspecting the Cause - -**Rule:** Use Cause to inspect, analyze, and handle all possible failure modes of an Effect, including expected errors, defects, and interruptions. - -**Skill Level:** advanced - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Cause, Effect } from "effect"; - -// An Effect that may fail with an error or defect -const program = Effect.try({ - try: () => { - throw new Error("Unexpected failure!"); - }, - catch: (err) => err, -}); - -// Catch all causes and inspect them -const handled = program.pipe( - Effect.catchAllCause((cause) => - Effect.sync(() => { - if (Cause.isDie(cause)) { - console.error("Defect (die):", Cause.pretty(cause)); - } else if (Cause.isFailure(cause)) { - console.error("Expected error:", Cause.pretty(cause)); - } else if (Cause.isInterrupted(cause)) { - console.error("Interrupted:", Cause.pretty(cause)); - } - // Handle or rethrow as needed - }) - ) -); -``` - -**Explanation:** - -- `Cause` distinguishes between expected errors (`fail`), defects (`die`), and interruptions. -- Use `Cause.pretty` for human-readable error traces. -- Enables advanced error handling and debugging. - -### Anti-Pattern - -Catching only expected errors and ignoring defects or interruptions, which can lead to silent failures, missed bugs, and harder debugging. - -### Explanation - -Traditional error handling often loses information about _why_ a failure occurred. -`Cause` preserves the full error context, enabling advanced debugging, error reporting, and robust recovery strategies. - ---- - -## Handle Your First Error with Effect.fail and catchAll - -**Rule:** Handle errors with Effect.fail and catchAll. - -**Skill Level:** beginner - -**Use Cases:** getting-started - -### Good Example - -```typescript -import { Effect, pipe } from "effect"; - -class UserNotFound { - readonly _tag = "UserNotFound"; - constructor(readonly id: string) {} -} - -const findUser = (id: string) => - id === "123" - ? Effect.succeed({ id, name: "Alice" }) - : Effect.fail(new UserNotFound(id)); - -const program = pipe( - findUser("456"), - Effect.catchTag("UserNotFound", (e) => - Effect.succeed({ id: e.id, name: "Guest" }) - ), - Effect.map((user) => `Hello, ${user.name}!`) -); - -const result = Effect.runSync(program); -console.log(result); // "Hello, Guest!" -``` - -### Explanation - -Real programs fail. Effect makes failures explicit in the type system so you -can't forget to handle them. Unlike try/catch, Effect errors are tracked in -types. - ---- - -## Handling Errors with catchAll, orElse, and match - -**Rule:** Use error handling combinators to recover from failures, provide fallback values, or transform errors in a composable way. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Option, Either } from "effect"; - -// Effect: Recover from any error -const effect = Effect.fail("fail!").pipe( - Effect.catchAll((err) => Effect.succeed(`Recovered from: ${err}`)) -); // Effect - -// Option: Provide a fallback if value is None -const option = Option.none().pipe(Option.orElse(() => Option.some("default"))); // Option - -// Either: Provide a fallback if value is Left -const either = Either.left("error").pipe( - Either.orElse(() => Either.right("fallback")) -); // Either - -// Effect: Pattern match on success or failure -const matchEffect = Effect.fail("fail!").pipe( - Effect.match({ - onFailure: (err) => `Error: ${err}`, - onSuccess: (value) => `Success: ${value}`, - }) -); // Effect -``` - -**Explanation:** -These combinators let you handle errors, provide defaults, or transform error values in a way that is composable and type-safe. -You can recover from errors, provide alternative computations, or pattern match on success/failure. - -### Anti-Pattern - -Using try/catch, null checks, or imperative error handling outside the combinator world. -This breaks composability, loses type safety, and makes error propagation unpredictable. - -### Explanation - -Error handling is a first-class concern in functional programming. -By using combinators, you keep error recovery logic close to where errors may occur, and avoid scattering try/catch or null checks throughout your code. - ---- - -## Handling Specific Errors with catchTag and catchTags - -**Rule:** Use catchTag and catchTags to handle specific tagged error types in the Effect failure channel, providing targeted recovery logic. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Effect, Data } from "effect"; - -// Define tagged error types -class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {} -class ValidationError extends Data.TaggedError("ValidationError")<{ - message: string; -}> {} - -type MyError = NotFoundError | ValidationError; - -// Effect: Handle only ValidationError, let others propagate -const effect = Effect.fail( - new ValidationError({ message: "Invalid input" }) as MyError -).pipe( - Effect.catchTag("ValidationError", (err) => - Effect.succeed(`Recovered from validation error: ${err.message}`) - ) -); // Effect - -// Effect: Handle multiple error tags -const effect2 = Effect.fail(new NotFoundError() as MyError).pipe( - Effect.catchTags({ - NotFoundError: () => Effect.succeed("Handled not found!"), - ValidationError: (err) => - Effect.succeed(`Handled validation: ${err.message}`), - }) -); // Effect -``` - -**Explanation:** - -- `catchTag` lets you recover from a specific tagged error type. -- `catchTags` lets you handle multiple tagged error types in one place. -- Unhandled errors continue to propagate, preserving error safety. - -### Anti-Pattern - -Catching all errors generically (e.g., with `catchAll`) and using manual type checks or property inspection, which is less safe and more error-prone than using tagged error combinators. - -### Explanation - -Not all errors should be handled the same way. -By matching on specific error tags, you can provide targeted recovery logic for each error type, while letting unhandled errors propagate as needed. - ---- - -## Hello World: Your First Effect - -**Rule:** Create your first Effect program with Effect.succeed. - -**Skill Level:** beginner - -**Use Cases:** getting-started - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Step 1: Create an Effect that succeeds with a value -const helloWorld = Effect.succeed("Hello, Effect!"); - -// Step 2: Run the Effect and get the result -const result = Effect.runSync(helloWorld); - -console.log(result); // "Hello, Effect!" -``` - -### Explanation - -Every journey starts with "Hello World". In Effect, you create computations -by describing what you want to happen, then you run them. This separation is -what makes Effect powerful. - ---- - -## Implement API Authentication - -**Rule:** Use middleware to validate authentication tokens before handling requests. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good Example - -```typescript -import { Effect, Context, Layer, Data } from "effect" -import { HttpServer, HttpServerRequest, HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. Define authentication types -// ============================================ - -interface User { - readonly id: string - readonly email: string - readonly roles: ReadonlyArray -} - -class AuthenticatedUser extends Context.Tag("AuthenticatedUser")< - AuthenticatedUser, - User ->() {} - -class UnauthorizedError extends Data.TaggedError("UnauthorizedError")<{ - readonly reason: string -}> {} - -class ForbiddenError extends Data.TaggedError("ForbiddenError")<{ - readonly requiredRole: string -}> {} - -// ============================================ -// 2. JWT validation service -// ============================================ - -interface JwtService { - readonly verify: (token: string) => Effect.Effect -} - -class Jwt extends Context.Tag("Jwt")() {} - -const JwtLive = Layer.succeed(Jwt, { - verify: (token) => - Effect.gen(function* () { - // In production: use a real JWT library - if (!token || token === "invalid") { - return yield* Effect.fail(new UnauthorizedError({ - reason: "Invalid or expired token" - })) - } - - // Decode token (simplified) - if (token.startsWith("user-")) { - return { - id: token.replace("user-", ""), - email: "user@example.com", - roles: ["user"], - } - } - - if (token.startsWith("admin-")) { - return { - id: token.replace("admin-", ""), - email: "admin@example.com", - roles: ["user", "admin"], - } - } - - return yield* Effect.fail(new UnauthorizedError({ - reason: "Malformed token" - })) - }), -}) - -// ============================================ -// 3. Authentication middleware -// ============================================ - -const extractBearerToken = (header: string | undefined): string | null => { - if (!header?.startsWith("Bearer ")) return null - return header.slice(7) -} - -const authenticate = ( - handler: Effect.Effect -): Effect.Effect => - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - const jwt = yield* Jwt - - const authHeader = request.headers["authorization"] - const token = extractBearerToken(authHeader) - - if (!token) { - return yield* Effect.fail(new UnauthorizedError({ - reason: "Missing Authorization header" - })) - } - - const user = yield* jwt.verify(token) - - return yield* handler.pipe( - Effect.provideService(AuthenticatedUser, user) - ) - }) - -// ============================================ -// 4. Role-based authorization -// ============================================ - -const requireRole = (role: string) => - (handler: Effect.Effect) => - Effect.gen(function* () { - const user = yield* AuthenticatedUser - - if (!user.roles.includes(role)) { - return yield* Effect.fail(new ForbiddenError({ requiredRole: role })) - } - - return yield* handler - }) - -// ============================================ -// 5. Protected routes -// ============================================ - -const getProfile = authenticate( - Effect.gen(function* () { - const user = yield* AuthenticatedUser - return HttpServerResponse.json({ - id: user.id, - email: user.email, - roles: user.roles, - }) - }) -) - -const adminOnly = authenticate( - requireRole("admin")( - Effect.gen(function* () { - const user = yield* AuthenticatedUser - return HttpServerResponse.json({ - message: `Welcome admin ${user.email}`, - users: ["user1", "user2", "user3"], - }) - }) - ) -) - -// ============================================ -// 6. Error handling -// ============================================ - -const handleAuthErrors = (effect: Effect.Effect) => - effect.pipe( - Effect.catchTag("UnauthorizedError", (e) => - Effect.succeed( - HttpServerResponse.json({ error: e.reason }, { status: 401 }) - ) - ), - Effect.catchTag("ForbiddenError", (e) => - Effect.succeed( - HttpServerResponse.json( - { error: `Requires role: ${e.requiredRole}` }, - { status: 403 } - ) - ) - ) - ) -``` - -### Explanation - -Authentication protects your API: - -1. **Identity verification** - Know who's making requests -2. **Access control** - Limit what users can do -3. **Audit trail** - Track who did what -4. **Rate limiting** - Per-user limits - ---- - ---- - -## Implement Backpressure in Pipelines - -**Rule:** Use buffering and throttling to handle producers faster than consumers. - -**Skill Level:** advanced - -**Use Cases:** building-data-pipelines - -### Good Example - -```typescript -import { Effect, Stream, Schedule, Duration, Queue, Chunk } from "effect" - -// ============================================ -// 1. Stream with natural backpressure -// ============================================ - -// Streams have built-in backpressure - consumers pull data -const fastProducer = Stream.fromIterable(Array.from({ length: 1000 }, (_, i) => i)) - -const slowConsumer = fastProducer.pipe( - Stream.tap((n) => - Effect.gen(function* () { - yield* Effect.sleep("10 millis") // Slow processing - yield* Effect.log(`Processed: ${n}`) - }) - ), - Stream.runDrain -) - -// Producer automatically slows down to match consumer - -// ============================================ -// 2. Explicit buffer with drop strategy -// ============================================ - -const bufferedStream = (source: Stream.Stream) => - source.pipe( - // Buffer up to 100 items, drop oldest when full - Stream.buffer({ capacity: 100, strategy: "dropping" }) - ) - -// ============================================ -// 3. Throttling - limit rate -// ============================================ - -const throttledStream = (source: Stream.Stream) => - source.pipe( - // Process at most 10 items per second - Stream.throttle({ - cost: () => 1, - units: 10, - duration: "1 second", - strategy: "enforce", - }) - ) - -// ============================================ -// 4. Debounce - wait for quiet period -// ============================================ - -const debouncedStream = (source: Stream.Stream) => - source.pipe( - // Wait 100ms of no new items before emitting - Stream.debounce("100 millis") - ) - -// ============================================ -// 5. Bounded queue for producer-consumer -// ============================================ - -const boundedQueueExample = Effect.gen(function* () { - // Create bounded queue - blocks producer when full - const queue = yield* Queue.bounded(10) - - // Fast producer - const producer = Effect.gen(function* () { - for (let i = 0; i < 100; i++) { - yield* Queue.offer(queue, i) - yield* Effect.log(`Produced: ${i}`) - } - yield* Queue.shutdown(queue) - }) - - // Slow consumer - const consumer = Effect.gen(function* () { - let count = 0 - while (true) { - const item = yield* Queue.take(queue).pipe( - Effect.catchTag("QueueShutdown", () => Effect.fail("done" as const)) - ) - if (item === "done") break - yield* Effect.sleep("50 millis") // Slow processing - yield* Effect.log(`Consumed: ${item}`) - count++ - } - return count - }).pipe(Effect.catchAll(() => Effect.succeed(0))) - - // Run both - producer will block when queue is full - yield* Effect.all([producer, consumer], { concurrency: 2 }) -}) - -// ============================================ -// 6. Sliding window - keep most recent -// ============================================ - -const slidingWindowStream = (source: Stream.Stream) => - source.pipe( - Stream.sliding(5), // Keep last 5 items - Stream.map((window) => ({ - items: window, - average: Chunk.reduce(window, 0, (a, b) => a + b) / Chunk.size(window), - })) - ) - -// ============================================ -// 7. Run example -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Backpressure Demo ===") - - // Throttled stream - const throttled = Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).pipe( - Stream.tap((n) => Effect.log(`Emitting: ${n}`)), - Stream.throttle({ - cost: () => 1, - units: 2, - duration: "1 second", - strategy: "enforce", - }), - Stream.tap((n) => Effect.log(`After throttle: ${n}`)), - Stream.runDrain - ) - - yield* throttled -}) - -Effect.runPromise(program) -``` - -### Explanation - -Backpressure prevents system overload: - -1. **Memory safety** - Don't buffer unlimited data -2. **Stability** - Slow consumers don't crash -3. **Fairness** - Distribute load appropriately -4. **Predictability** - Consistent performance - ---- - ---- - -## Implement Dead Letter Queues - -**Rule:** Capture failed items with context for debugging and retry instead of losing them. - -**Skill Level:** advanced - -**Use Cases:** building-data-pipelines - -### Good Example - -```typescript -import { Effect, Stream, Queue, Chunk, Ref, Data } from "effect" - -// ============================================ -// 1. Define DLQ types -// ============================================ - -interface DeadLetterItem { - readonly item: T - readonly error: unknown - readonly timestamp: Date - readonly attempts: number - readonly context: Record -} - -interface ProcessingResult { - readonly _tag: "Success" | "Failure" -} - -class Success implements ProcessingResult { - readonly _tag = "Success" - constructor( - readonly item: T, - readonly result: R - ) {} -} - -class Failure implements ProcessingResult { - readonly _tag = "Failure" - constructor( - readonly item: T, - readonly error: unknown, - readonly attempts: number - ) {} -} - -// ============================================ -// 2. Create a DLQ service -// ============================================ - -const makeDLQ = () => - Effect.gen(function* () { - const queue = yield* Queue.unbounded>() - const countRef = yield* Ref.make(0) - - return { - send: (item: T, error: unknown, attempts: number, context: Record = {}) => - Effect.gen(function* () { - yield* Queue.offer(queue, { - item, - error, - timestamp: new Date(), - attempts, - context, - }) - yield* Ref.update(countRef, (n) => n + 1) - yield* Effect.log(`DLQ: Added item (total: ${(yield* Ref.get(countRef))})`) - }), - - getAll: () => - Effect.gen(function* () { - const items: DeadLetterItem[] = [] - while (!(yield* Queue.isEmpty(queue))) { - const item = yield* Queue.poll(queue) - if (item._tag === "Some") { - items.push(item.value) - } - } - return items - }), - - count: () => Ref.get(countRef), - - queue, - } - }) - -// ============================================ -// 3. Process with DLQ -// ============================================ - -interface Order { - id: string - amount: number -} - -const processOrder = (order: Order): Effect.Effect => - Effect.gen(function* () { - // Simulate random failures - if (order.amount < 0) { - return yield* Effect.fail(new Error("Invalid amount")) - } - if (order.id === "fail") { - return yield* Effect.fail(new Error("Processing failed")) - } - yield* Effect.sleep("10 millis") - return `Processed order ${order.id}: $${order.amount}` - }) - -const processWithRetryAndDLQ = ( - orders: Stream.Stream, - maxRetries: number = 3 -) => - Effect.gen(function* () { - const dlq = yield* makeDLQ() - - const results = yield* orders.pipe( - Stream.mapEffect((order) => - Effect.gen(function* () { - let lastError: unknown - for (let attempt = 1; attempt <= maxRetries; attempt++) { - const result = yield* processOrder(order).pipe( - Effect.map((r) => new Success(order, r)), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`Attempt ${attempt}/${maxRetries} failed for ${order.id}`) - lastError = error - if (attempt < maxRetries) { - yield* Effect.sleep("100 millis") // Backoff - } - return new Failure(order, error, attempt) as ProcessingResult - }) - ) - ) - - if (result._tag === "Success") { - return result - } - } - - // All retries exhausted - send to DLQ - yield* dlq.send(order, lastError, maxRetries, { orderId: order.id }) - return new Failure(order, lastError, maxRetries) - }) - ), - Stream.runCollect - ) - - const successful = Chunk.filter(results, (r): r is Success => r._tag === "Success") - const failed = Chunk.filter(results, (r): r is Failure => r._tag === "Failure") - - yield* Effect.log(`\nResults: ${Chunk.size(successful)} success, ${Chunk.size(failed)} failed`) - - // Get DLQ contents - const dlqItems = yield* dlq.getAll() - if (dlqItems.length > 0) { - yield* Effect.log("\n=== Dead Letter Queue Contents ===") - for (const item of dlqItems) { - yield* Effect.log( - `- Order ${item.item.id}: ${item.error} (attempts: ${item.attempts})` - ) - } - } - - return { successful, failed, dlqItems } - }) - -// ============================================ -// 4. DLQ reprocessing -// ============================================ - -const reprocessDLQ = ( - dlqItems: DeadLetterItem[], - processor: (item: T) => Effect.Effect -) => - Effect.gen(function* () { - yield* Effect.log(`Reprocessing ${dlqItems.length} DLQ items...`) - - for (const dlqItem of dlqItems) { - const result = yield* processor(dlqItem.item).pipe( - Effect.map(() => "success" as const), - Effect.catchAll(() => Effect.succeed("failed" as const)) - ) - - yield* Effect.log( - `Reprocess ${JSON.stringify(dlqItem.item)}: ${result}` - ) - } - }) - -// ============================================ -// 5. Run example -// ============================================ - -const program = Effect.gen(function* () { - const orders: Order[] = [ - { id: "1", amount: 100 }, - { id: "2", amount: 200 }, - { id: "fail", amount: 50 }, // Will fail all retries - { id: "3", amount: 300 }, - { id: "4", amount: -10 }, // Invalid amount - { id: "5", amount: 150 }, - ] - - yield* Effect.log("=== Processing Orders ===\n") - const { dlqItems } = yield* processWithRetryAndDLQ(Stream.fromIterable(orders), 3) - - if (dlqItems.length > 0) { - yield* Effect.log("\n=== Attempting DLQ Reprocessing ===") - yield* reprocessDLQ(dlqItems, (order) => - Effect.gen(function* () { - yield* Effect.log(`Manual fix for order ${order.id}`) - }) - ) - } -}) - -Effect.runPromise(program) -``` - -### Explanation - -Dead letter queues provide: - -1. **Resilience** - Pipeline continues despite failures -2. **Visibility** - See what's failing and why -3. **Recovery** - Reprocess failed items later -4. **Debugging** - Error context for investigation - ---- - ---- - -## Implement Distributed Tracing - -**Rule:** Propagate trace context across service boundaries to correlate requests. - -**Skill Level:** advanced - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, Context, Layer } from "effect" -import { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "@effect/platform" - -// ============================================ -// 1. Define trace context -// ============================================ - -interface TraceContext { - readonly traceId: string - readonly spanId: string - readonly parentSpanId?: string - readonly sampled: boolean -} - -class CurrentTrace extends Context.Tag("CurrentTrace")< - CurrentTrace, - TraceContext ->() {} - -// W3C Trace Context header names -const TRACEPARENT_HEADER = "traceparent" -const TRACESTATE_HEADER = "tracestate" - -// ============================================ -// 2. Generate trace IDs -// ============================================ - -const generateTraceId = (): string => - Array.from(crypto.getRandomValues(new Uint8Array(16))) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - -const generateSpanId = (): string => - Array.from(crypto.getRandomValues(new Uint8Array(8))) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - -// ============================================ -// 3. Parse and format trace context -// ============================================ - -const parseTraceparent = (header: string): TraceContext | null => { - // Format: 00-traceId-spanId-flags - const parts = header.split("-") - if (parts.length !== 4) return null - - return { - traceId: parts[1], - spanId: generateSpanId(), // New span for this service - parentSpanId: parts[2], - sampled: parts[3] === "01", - } -} - -const formatTraceparent = (ctx: TraceContext): string => - `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}` - -// ============================================ -// 4. Extract trace from incoming request -// ============================================ - -const extractTraceContext = Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - - const traceparent = request.headers[TRACEPARENT_HEADER] - - if (traceparent) { - const parsed = parseTraceparent(traceparent) - if (parsed) { - yield* Effect.log("Extracted trace context").pipe( - Effect.annotateLogs({ - traceId: parsed.traceId, - parentSpanId: parsed.parentSpanId, - }) - ) - return parsed - } - } - - // No incoming trace - start a new one - const newTrace: TraceContext = { - traceId: generateTraceId(), - spanId: generateSpanId(), - sampled: Math.random() < 0.1, // 10% sampling - } - - yield* Effect.log("Started new trace").pipe( - Effect.annotateLogs({ traceId: newTrace.traceId }) - ) - - return newTrace -}) - -// ============================================ -// 5. Propagate trace to outgoing requests -// ============================================ - -const makeTracedHttpClient = Effect.gen(function* () { - const baseClient = yield* HttpClient.HttpClient - const trace = yield* CurrentTrace - - return { - get: (url: string) => - Effect.gen(function* () { - // Create child span for outgoing request - const childSpan: TraceContext = { - traceId: trace.traceId, - spanId: generateSpanId(), - parentSpanId: trace.spanId, - sampled: trace.sampled, - } - - yield* Effect.log("Making traced HTTP request").pipe( - Effect.annotateLogs({ - traceId: childSpan.traceId, - spanId: childSpan.spanId, - url, - }) - ) - - const request = HttpClientRequest.get(url).pipe( - HttpClientRequest.setHeader( - TRACEPARENT_HEADER, - formatTraceparent(childSpan) - ) - ) - - return yield* baseClient.execute(request) - }), - } -}) - -// ============================================ -// 6. Tracing middleware for HTTP server -// ============================================ - -const withTracing = ( - handler: Effect.Effect -): Effect.Effect => - Effect.gen(function* () { - const traceContext = yield* extractTraceContext - - return yield* handler.pipe( - Effect.provideService(CurrentTrace, traceContext), - Effect.withLogSpan(`request-${traceContext.spanId}`), - Effect.annotateLogs({ - "trace.id": traceContext.traceId, - "span.id": traceContext.spanId, - "parent.span.id": traceContext.parentSpanId ?? "none", - }) - ) - }) - -// ============================================ -// 7. Example: Service A calls Service B -// ============================================ - -// Service B handler -const serviceBHandler = withTracing( - Effect.gen(function* () { - const trace = yield* CurrentTrace - yield* Effect.log("Service B processing request") - - // Simulate work - yield* Effect.sleep("50 millis") - - return HttpServerResponse.json({ - message: "Hello from Service B", - traceId: trace.traceId, - }) - }) -) - -// Service A handler (calls Service B) -const serviceAHandler = withTracing( - Effect.gen(function* () { - const trace = yield* CurrentTrace - yield* Effect.log("Service A processing request") - - // Call Service B with trace propagation - const tracedClient = yield* makeTracedHttpClient - const response = yield* tracedClient.get("http://service-b/api/data") - - yield* Effect.log("Service A received response from B") - - return HttpServerResponse.json({ - message: "Hello from Service A", - traceId: trace.traceId, - }) - }) -) - -// ============================================ -// 8. Run and observe -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Distributed Tracing Demo ===") - - // Simulate incoming request with trace - const incomingTrace: TraceContext = { - traceId: generateTraceId(), - spanId: generateSpanId(), - sampled: true, - } - - yield* Effect.log("Processing traced request").pipe( - Effect.provideService(CurrentTrace, incomingTrace), - Effect.annotateLogs({ - "trace.id": incomingTrace.traceId, - "span.id": incomingTrace.spanId, - }) - ) -}) - -Effect.runPromise(program) -``` - -### Explanation - -Distributed tracing shows the complete request journey: - -1. **End-to-end visibility** - See entire request flow -2. **Latency analysis** - Find slow services -3. **Error correlation** - Link errors across services -4. **Dependency mapping** - Understand service relationships - ---- - ---- - -## Implement Graceful Shutdown for Your Application - -**Rule:** Use Effect.runFork and OS signal listeners to implement graceful shutdown for long-running applications. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This example creates a server with a "scoped" database connection. It uses `runFork` to start the server and sets up a `SIGINT` handler to interrupt the server fiber, which in turn guarantees the database finalizer is called. - -```typescript -import { Effect, Layer, Fiber, Context, Scope } from "effect"; -import * as http from "http"; - -// 1. A service with a finalizer for cleanup -class Database extends Effect.Service()("Database", { - effect: Effect.gen(function* () { - yield* Effect.log("Acquiring DB connection"); - return { - query: () => Effect.succeed("data"), - }; - }), -}) {} - -// 2. The main server logic -const server = Effect.gen(function* () { - const db = yield* Database; - - // Create server with proper error handling - const httpServer = yield* Effect.sync(() => { - const server = http.createServer((_req, res) => { - Effect.runFork( - Effect.provide( - db.query().pipe(Effect.map((data) => res.end(data))), - Database.Default - ) - ); - }); - return server; - }); - - // Add a finalizer to close the server - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - httpServer.close(); - yield* Effect.log("Server closed"); - }) - ); - - // Start server with error handling - yield* Effect.async((resume) => { - httpServer.once("error", (err: Error) => { - resume(Effect.fail(new Error(`Failed to start server: ${err.message}`))); - }); - - httpServer.listen(3456, () => { - resume(Effect.succeed(void 0)); - }); - }); - - yield* Effect.log("Server started on port 3456. Press Ctrl+C to exit."); - - // For testing purposes, we'll run for a short time instead of forever - yield* Effect.sleep("2 seconds"); - yield* Effect.log("Shutting down gracefully..."); -}); - -// 3. Provide the layer and launch with runFork -const app = Effect.provide(server.pipe(Effect.scoped), Database.Default); - -// 4. Run the app and handle shutdown -Effect.runPromise(app).catch((error) => { - Effect.runSync(Effect.logError("Application error: " + error)); - process.exit(1); -}); -``` - ---- - -### Anti-Pattern - -Letting the Node.js process exit without proper cleanup. If you run a long-running effect with `Effect.runPromise` or don't handle OS signals, pressing Ctrl+C will terminate the process abruptly, and none of your `Effect` finalizers will have a chance to run. - -```typescript -import { Effect } from "effect"; -import { app } from "./somewhere"; // From previous example - -// ❌ WRONG: This will run the server, but Ctrl+C will kill it instantly. -// The database connection finalizer will NOT be called. -Effect.runPromise(app); -``` - -### Explanation - -When a server process is terminated, you need to ensure that it cleans up properly. This includes closing database connections, finishing in-flight requests, and releasing file handles. Failing to do so can lead to resource leaks or data corruption. - -Effect's structured concurrency makes this robust and easy. When a fiber is interrupted, Effect guarantees that it will run all finalizers registered within that fiber's scope, in the reverse order they were acquired. - -By launching your app with `runFork`, you get a `Fiber` that represents the entire application. Triggering `Fiber.interrupt` on this top-level fiber initiates a clean, orderly shutdown sequence for all its resources. - ---- - ---- - -## Instrument and Observe Function Calls with Effect.fn - -**Rule:** Use Effect.fn to wrap functions with effectful instrumentation, such as logging, metrics, or tracing, in a composable and type-safe way. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect } from "effect"; - -// A simple function to instrument -function add(a: number, b: number): number { - return a + b; -} - -// Use Effect.fn to instrument the function with observability -const addWithLogging = Effect.fn("add")(add).pipe( - Effect.withSpan("add", { attributes: { "fn.name": "add" } }) -); - -// Use the instrumented function in an Effect workflow -const program = Effect.gen(function* () { - yield* Effect.logInfo("Calling add function"); - const sum = yield* addWithLogging(2, 3); - yield* Effect.logInfo(`Sum is ${sum}`); - return sum; -}); - -// Run the program -Effect.runPromise(program); -``` - -**Explanation:** - -- `Effect.fn("name")(fn)` wraps a function with instrumentation capabilities, enabling observability. -- You can add tracing spans, logging, metrics, and other observability logic to function boundaries. -- Keeps instrumentation separate from business logic and fully composable. -- The wrapped function integrates seamlessly with Effect's observability and tracing infrastructure. - -### Anti-Pattern - -Scattering logging, metrics, or tracing logic directly inside business functions, making code harder to test, maintain, and compose. - -### Explanation - -Instrumenting function calls is essential for observability, especially in complex or critical code paths. -`Effect.fn` lets you add effectful logic (logging, metrics, tracing, etc.) before, after, or around any function call, without changing the function’s core logic. - ---- - -## Integrate Effect Tracing with OpenTelemetry - -**Rule:** Integrate Effect.withSpan with OpenTelemetry to export traces and visualize request flows across services. - -**Skill Level:** advanced - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect } from "effect"; -// Pseudocode: Replace with actual OpenTelemetry integration for your stack -import { trace, context, SpanStatusCode } from "@opentelemetry/api"; - -// Wrap an Effect.withSpan to export to OpenTelemetry -function withOtelSpan( - name: string, - effect: Effect.Effect -) { - return Effect.gen(function* () { - const otelSpan = trace.getTracer("default").startSpan(name); - try { - const result = yield* effect; - otelSpan.setStatus({ code: SpanStatusCode.OK }); - return result; - } catch (err) { - otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(err) }); - throw err; - } finally { - otelSpan.end(); - } - }); -} - -// Usage -const program = withOtelSpan( - "fetchUser", - Effect.sync(() => { - // ...fetch user logic - return { id: 1, name: "Alice" }; - }) -); -``` - -**Explanation:** - -- Start an OpenTelemetry span when entering an Effectful operation. -- Set status and attributes as needed. -- End the span when the operation completes or fails. -- This enables full distributed tracing and visualization in your observability platform. - -### Anti-Pattern - -Using Effect.withSpan without exporting to OpenTelemetry, or lacking distributed tracing, which limits your ability to diagnose and visualize complex request flows. - -### Explanation - -OpenTelemetry is the industry standard for distributed tracing. -By integrating Effect's spans with OpenTelemetry, you gain deep visibility into request flows, performance bottlenecks, and dependencies—across all your services and infrastructure. - ---- - -## Leverage Effect's Built-in Structured Logging - -**Rule:** Use Effect.log, Effect.logInfo, and Effect.logError to add structured, context-aware logging to your Effect code. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Log a simple message -const program = Effect.gen(function* () { - yield* Effect.log("Starting the application"); -}); - -// Log at different levels -const infoProgram = Effect.gen(function* () { - yield* Effect.logInfo("User signed in"); -}); - -const errorProgram = Effect.gen(function* () { - yield* Effect.logError("Failed to connect to database"); -}); - -// Log with dynamic values -const userId = 42; -const logUserProgram = Effect.gen(function* () { - yield* Effect.logInfo(`Processing user: ${userId}`); -}); - -// Use logging in a workflow -const workflow = Effect.gen(function* () { - yield* Effect.log("Beginning workflow"); - // ... do some work - yield* Effect.logInfo("Workflow step completed"); - // ... handle errors - yield* Effect.logError("Something went wrong"); -}); -``` - -**Explanation:** - -- `Effect.log` logs a message at the default level. -- `Effect.logInfo` and `Effect.logError` log at specific levels. -- Logging is context-aware and can be used anywhere in your Effect workflows. - -### Anti-Pattern - -Using `console.log` or ad-hoc logging scattered throughout your code, which is not structured, not context-aware, and harder to manage in production. - -### Explanation - -Structured logging makes it easier to search, filter, and analyze logs in production. -Effect’s logging functions are context-aware, meaning they automatically include relevant metadata and can be configured globally. - ---- - -## Leverage Effect's Built-in Structured Logging - -**Rule:** Leverage Effect's built-in structured logging. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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. - -### Anti-Pattern - -Calling `console.log` directly within an Effect composition. This is an -unmanaged side-effect that bypasses all the benefits of Effect's logging system. - -### Explanation - -Effect's logger is structured, context-aware (with trace IDs), configurable -via `Layer`, and testable. It's a first-class citizen, not an unmanaged -side-effect. - ---- - -## Lifting Errors and Absence with fail, none, and left - -**Rule:** Use fail, none, and left to create Effect, Option, or Either that represent failure or absence. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Option, Either } from "effect"; - -// Effect: Represent a failure with an error value -const effect = Effect.fail("Something went wrong"); // Effect - -// Option: Represent absence of a value -const option = Option.none(); // Option - -// Either: Represent a failure with a left value -const either = Either.left("Invalid input"); // Either -``` - -**Explanation:** - -- `Effect.fail(error)` creates an effect that always fails with `error`. -- `Option.none()` creates an option that is always absent. -- `Either.left(error)` creates an either that always represents failure. - -### Anti-Pattern - -Throwing exceptions, returning `null` or `undefined`, or using error codes outside the Effect, Option, or Either world. -This makes error handling ad hoc, less type-safe, and harder to compose. - -### Explanation - -By lifting errors and absence into these structures, you can handle them declaratively with combinators, rather than relying on exceptions, `null`, or `undefined`. -This leads to more robust and maintainable code. - ---- - -## Lifting Values with succeed, some, and right - -**Rule:** Use succeed, some, and right to create Effect, Option, or Either from plain values. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Option, Either } from "effect"; - -// Effect: Lift a value into an Effect that always succeeds -const effect = Effect.succeed(42); // Effect - -// Option: Lift a value into an Option that is always Some -const option = Option.some("hello"); // Option - -// Either: Lift a value into an Either that is always Right -const either = Either.right({ id: 1 }); // Either -``` - -**Explanation:** - -- `Effect.succeed(value)` creates an effect that always succeeds with `value`. -- `Option.some(value)` creates an option that is always present. -- `Either.right(value)` creates an either that always represents success. - -### Anti-Pattern - -Passing plain values around outside the Effect, Option, or Either world, or using `null`/`undefined` to represent absence or success. -This leads to less composable, less type-safe code and makes error handling harder. - -### Explanation - -Lifting values into these structures allows you to compose them with other effects, options, or eithers, and to take advantage of all the combinators and error handling that Effect provides. - ---- - -## Log HTTP Requests and Responses - -**Rule:** Use Effect's logging to trace HTTP requests for debugging and monitoring. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Duration } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" - -// ============================================ -// 1. Simple request/response logging -// ============================================ - -const withLogging = ( - request: Effect.Effect -): Effect.Effect => - Effect.gen(function* () { - const startTime = Date.now() - yield* Effect.log("→ HTTP Request starting...") - - const result = yield* request - - const duration = Date.now() - startTime - yield* Effect.log(`← HTTP Response received (${duration}ms)`) - - return result - }) - -// ============================================ -// 2. Detailed request logging -// ============================================ - -interface RequestLog { - method: string - url: string - headers: Record - body?: unknown -} - -interface ResponseLog { - status: number - headers: Record - duration: number - size?: number -} - -const makeLoggingClient = Effect.gen(function* () { - const baseClient = yield* HttpClient.HttpClient - - const logRequest = (method: string, url: string, headers: Record) => - Effect.log("HTTP Request").pipe( - Effect.annotateLogs({ - method, - url, - headers: JSON.stringify(headers), - }) - ) - - const logResponse = (status: number, duration: number, headers: Record) => - Effect.log("HTTP Response").pipe( - Effect.annotateLogs({ - status: String(status), - duration: `${duration}ms`, - headers: JSON.stringify(headers), - }) - ) - - return { - get: (url: string, options?: { headers?: Record }) => - Effect.gen(function* () { - const headers = options?.headers ?? {} - yield* logRequest("GET", url, headers) - const startTime = Date.now() - - const response = yield* baseClient.get(url) - - yield* logResponse( - response.status, - Date.now() - startTime, - response.headers - ) - - return yield* HttpClientResponse.json(response) as Effect.Effect - }), - - post: (url: string, body: unknown, options?: { headers?: Record }) => - Effect.gen(function* () { - const headers = options?.headers ?? {} - yield* logRequest("POST", url, headers).pipe( - Effect.annotateLogs("body", JSON.stringify(body).slice(0, 200)) - ) - const startTime = Date.now() - - const request = yield* HttpClientRequest.post(url).pipe( - HttpClientRequest.jsonBody(body) - ) - const response = yield* baseClient.execute(request) - - yield* logResponse( - response.status, - Date.now() - startTime, - response.headers - ) - - return yield* HttpClientResponse.json(response) as Effect.Effect - }), - } -}) - -// ============================================ -// 3. Log with span for timing -// ============================================ - -const fetchWithSpan = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.withLogSpan(`HTTP GET ${url}`) - ) - }) - -// ============================================ -// 4. Conditional logging (debug mode) -// ============================================ - -const makeConditionalLoggingClient = (debug: boolean) => - Effect.gen(function* () { - const baseClient = yield* HttpClient.HttpClient - - const maybeLog = (message: string, data?: Record) => - debug - ? Effect.log(message).pipe( - data ? Effect.annotateLogs(data) : (e) => e - ) - : Effect.void - - return { - get: (url: string) => - Effect.gen(function* () { - yield* maybeLog("HTTP Request", { method: "GET", url }) - const startTime = Date.now() - - const response = yield* baseClient.get(url) - - yield* maybeLog("HTTP Response", { - status: String(response.status), - duration: `${Date.now() - startTime}ms`, - }) - - return yield* HttpClientResponse.json(response) as Effect.Effect - }), - } - }) - -// ============================================ -// 5. Request ID tracking -// ============================================ - -const makeTrackedClient = Effect.gen(function* () { - const baseClient = yield* HttpClient.HttpClient - - return { - get: (url: string) => - Effect.gen(function* () { - const requestId = crypto.randomUUID().slice(0, 8) - - yield* Effect.log("HTTP Request").pipe( - Effect.annotateLogs({ - requestId, - method: "GET", - url, - }) - ) - - const startTime = Date.now() - const response = yield* baseClient.get(url) - - yield* Effect.log("HTTP Response").pipe( - Effect.annotateLogs({ - requestId, - status: String(response.status), - duration: `${Date.now() - startTime}ms`, - }) - ) - - return yield* HttpClientResponse.json(response) as Effect.Effect - }) - } -}) - -// ============================================ -// 6. Error logging -// ============================================ - -const fetchWithErrorLogging = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((response) => { - if (response.status >= 400) { - return Effect.gen(function* () { - yield* Effect.logError("HTTP Error").pipe( - Effect.annotateLogs({ - url, - status: String(response.status), - }) - ) - return yield* Effect.fail(new Error(`HTTP ${response.status}`)) - }) - } - return Effect.succeed(response) - }), - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.tapError((error) => - Effect.logError("Request failed").pipe( - Effect.annotateLogs({ - url, - error: String(error), - }) - ) - ) - ) - }) - -// ============================================ -// 7. Usage -// ============================================ - -const program = Effect.gen(function* () { - const client = yield* makeLoggingClient - - yield* Effect.log("Starting HTTP operations...") - - const data = yield* client.get("https://api.example.com/users") - - yield* Effect.log("Operations complete") -}) -``` - -### Explanation - -HTTP logging helps with: - -1. **Debugging** - See what's being sent/received -2. **Performance** - Track slow requests -3. **Auditing** - Record API usage -4. **Troubleshooting** - Diagnose production issues - ---- - ---- - -## 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. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good 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* Effect.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* Effect.log("Server is running and ready to handle requests"); - yield* Effect.sleep(Duration.seconds(2)); - - // Shutdown gracefully - yield* Fiber.interrupt(serverFiber); - yield* Effect.log("Server shutdown complete"); -}); - -NodeRuntime.runMain( - Effect.provide( - program, - Layer.provide(serverLayer, Layer.merge(Database.Default, server)) - ) as Effect.Effect -); -``` - -### Anti-Pattern - -The anti-pattern is to use `fetch` inside a route handler, wrapped in `Effect.tryPromise`. This approach requires manual error handling and loses the benefits of the Effect ecosystem. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -const proxyRoute = Http.router.get( - "/proxy/posts/:id", - Effect.flatMap(Http.request.ServerRequest, (req) => - // Manually wrap fetch in an Effect - Effect.tryPromise({ - try: () => - fetch(`https://jsonplaceholder.typicode.com/posts/${req.params.id}`), - catch: () => "FetchError", // Untyped error - }).pipe( - Effect.flatMap((res) => - // Manually check status and parse JSON, each with its own error case - res.ok - ? Effect.tryPromise({ - try: () => res.json(), - catch: () => "JsonError", - }) - : Effect.fail("BadStatusError") - ), - Effect.map(Http.response.json), - // A generic catch-all because we can't easily distinguish error types - Effect.catchAll(() => - Http.response.text("An unknown error occurred", { status: 500 }) - ) - ) - ) -); - -const app = Http.router.empty.pipe(Http.router.addRoute(proxyRoute)); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This manual approach is significantly more complex and less safe. It forces you to reinvent status and parsing logic, uses untyped string-based errors, and most importantly, the `fetch` call will not be automatically interrupted if the parent request is cancelled. - -### Explanation - -An API server often needs to communicate with other services. While you could use the native `fetch` API, this breaks out of the Effect ecosystem and forfeits its most powerful features. Using the built-in `Http.client` is superior for several critical reasons: - -1. **Full Integration**: An `Http.client` request is a first-class `Effect`. This means it seamlessly composes with all other effects. You can add timeouts, retry logic (`Schedule`), or race it with other operations using the standard Effect operators you already know. -2. **Structured Concurrency**: This is a key benefit. If the original incoming request to your server is cancelled or times out, Effect will automatically interrupt the outgoing `Http.client` request. A raw `fetch` call would continue running in the background, wasting resources. -3. **Typed Errors**: The client provides a rich set of typed errors (e.g., `Http.error.RequestError`, `Http.error.ResponseError`). This allows you to write precise error handling logic to distinguish between a network failure and a non-2xx response from the external API. -4. **Testability**: The `Http.client` can be provided via a `Layer`, making it trivial to mock in tests. You can test your route's logic without making actual network calls, leading to faster and more reliable tests. - ---- - ---- - -## Manage Hierarchical Resources - -**Rule:** Use nested Scopes to manage resources with parent-child dependencies. - -**Skill Level:** advanced - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Scope, Exit } from "effect" - -// ============================================ -// 1. Define hierarchical resources -// ============================================ - -interface Database { - readonly name: string - readonly createConnection: () => Effect.Effect -} - -interface Connection { - readonly id: string - readonly database: string - readonly beginTransaction: () => Effect.Effect -} - -interface Transaction { - readonly id: string - readonly connectionId: string - readonly execute: (sql: string) => Effect.Effect -} - -// ============================================ -// 2. Create resources with proper lifecycle -// ============================================ - -const makeDatabase = (name: string): Effect.Effect => - Effect.acquireRelease( - Effect.gen(function* () { - yield* Effect.log(`Opening database: ${name}`) - - const db: Database = { - name, - createConnection: () => makeConnection(name), - } - - return db - }), - (db) => Effect.log(`Closing database: ${db.name}`) - ) - -const makeConnection = (dbName: string): Effect.Effect => - Effect.acquireRelease( - Effect.gen(function* () { - const id = `conn-${crypto.randomUUID().slice(0, 8)}` - yield* Effect.log(` Opening connection: ${id} to ${dbName}`) - - const conn: Connection = { - id, - database: dbName, - beginTransaction: () => makeTransaction(id), - } - - return conn - }), - (conn) => Effect.log(` Closing connection: ${conn.id}`) - ) - -const makeTransaction = (connId: string): Effect.Effect => - Effect.acquireRelease( - Effect.gen(function* () { - const id = `tx-${crypto.randomUUID().slice(0, 8)}` - yield* Effect.log(` Beginning transaction: ${id}`) - - const tx: Transaction = { - id, - connectionId: connId, - execute: (sql) => Effect.log(` [${id}] ${sql}`), - } - - return tx - }), - (tx) => Effect.log(` Committing transaction: ${tx.id}`) - ) - -// ============================================ -// 3. Use hierarchical resources -// ============================================ - -const program = Effect.scoped( - Effect.gen(function* () { - yield* Effect.log("=== Starting hierarchical resource demo ===\n") - - // Level 1: Database - const db = yield* makeDatabase("myapp") - - // Level 2: Connection (child of database) - const conn = yield* db.createConnection() - - // Level 3: Transaction (child of connection) - const tx = yield* conn.beginTransaction() - - // Use the transaction - yield* tx.execute("INSERT INTO users (name) VALUES ('Alice')") - yield* tx.execute("INSERT INTO users (name) VALUES ('Bob')") - - yield* Effect.log("\n=== Work complete, releasing resources ===\n") - - // Resources released in reverse order: - // 1. Transaction committed - // 2. Connection closed - // 3. Database closed - }) -) - -Effect.runPromise(program) - -// ============================================ -// 4. Multiple children at same level -// ============================================ - -const multipleConnections = Effect.scoped( - Effect.gen(function* () { - const db = yield* makeDatabase("myapp") - - // Create multiple connections - const conn1 = yield* db.createConnection() - const conn2 = yield* db.createConnection() - - // Each connection can have transactions - const tx1 = yield* conn1.beginTransaction() - const tx2 = yield* conn2.beginTransaction() - - // Use both transactions - yield* Effect.all([ - tx1.execute("UPDATE table1 SET x = 1"), - tx2.execute("UPDATE table2 SET y = 2"), - ]) - - // All released in proper order - }) -) -``` - -### Explanation - -Resources often have dependencies: - -1. **Database → Connections → Transactions** - Transaction needs connection -2. **Server → Routes → Handlers** - Handler needs server context -3. **File → Reader → Parser** - Parser needs reader - -Release order matters: children before parents. - ---- - ---- - -## Manage Resource Lifecycles with Scope - -**Rule:** Use Scope for fine-grained, manual control over resource lifecycles and cleanup guarantees. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This example shows how to acquire a resource (like a file handle), use it, and have `Scope` guarantee its release. - -```typescript -import { Effect, Scope } from "effect"; - -// Simulate acquiring and releasing a resource -const acquireFile = Effect.log("File opened").pipe( - Effect.as({ write: (data: string) => Effect.log(`Wrote: ${data}`) }) -); -const releaseFile = Effect.log("File closed."); - -// Create a "scoped" effect. This effect, when used, will acquire the -// resource and register its release action with the current scope. -const scopedFile = Effect.acquireRelease(acquireFile, () => releaseFile); - -// The main program that uses the scoped resource -const program = Effect.gen(function* () { - // Effect.scoped "uses" the resource. It runs the acquire effect, - // provides the resource to the inner effect, and ensures the - // release effect is run when this block completes. - const file = yield* Effect.scoped(scopedFile); - - yield* file.write("hello"); - yield* file.write("world"); - - // The file will be automatically closed here. -}); - -Effect.runPromise(program); -/* -Output: -File opened -Wrote: hello -Wrote: world -File closed -*/ -``` - ---- - -### Anti-Pattern - -Manual resource management without the guarantees of `Scope`. This is brittle because if an error occurs after the resource is acquired but before it's released, the release logic is never executed. - -```typescript -import { Effect } from "effect"; -import { acquireFile, releaseFile } from "./somewhere"; // From previous example - -// ❌ WRONG: This will leak the resource if an error happens. -const program = Effect.gen(function* () { - const file = yield* acquireFile; - - // If this operation fails... - yield* Effect.fail("Something went wrong!"); - - // ...this line will never be reached, and the file will never be closed. - yield* releaseFile; -}); -``` - -### Explanation - -`Scope` is the fundamental building block for all resource management in Effect. While higher-level APIs like `Layer.scoped` and `Stream` are often sufficient, understanding `Scope` is key to advanced use cases. - -A `Scope` guarantees that any finalizers added to it will be executed when the scope is closed, regardless of whether the associated computation succeeds, fails, or is interrupted. This provides a rock-solid guarantee against resource leaks. - -This is especially critical in concurrent applications. When a parent fiber is interrupted, it closes its scope, which in turn automatically interrupts all its child fibers and runs all their finalizers in a structured, predictable order. - ---- - ---- - -## Manage Resources Safely in a Pipeline - -**Rule:** Use Stream.acquireRelease to safely manage the lifecycle of a resource within a pipeline. - -**Skill Level:** advanced - -**Use Cases:** building-data-pipelines - -### Good Example - -This example creates and writes to a temporary file. `Stream.acquireRelease` is used to acquire a readable stream from that file. The pipeline then processes the file but is designed to fail partway through. The logs demonstrate that the `release` effect (which deletes the file) is still executed, preventing any resource leaks. - -```typescript -import { Effect, Layer } from "effect"; -import { FileSystem } from "@effect/platform/FileSystem"; -import { NodeFileSystem } from "@effect/platform-node"; -import * as path from "node:path"; - -interface ProcessError { - readonly _tag: "ProcessError"; - readonly message: string; -} - -const ProcessError = (message: string): ProcessError => ({ - _tag: "ProcessError", - message, -}); - -interface FileServiceType { - readonly createTempFile: () => Effect.Effect<{ filePath: string }, never>; - readonly cleanup: (filePath: string) => Effect.Effect; - readonly readFile: (filePath: string) => Effect.Effect; -} - -export class FileService extends Effect.Service()("FileService", { - sync: () => { - const filePath = path.join(__dirname, "temp-resource.txt"); - return { - createTempFile: () => Effect.succeed({ filePath }), - cleanup: (filePath: string) => - Effect.log("✅ Resource cleaned up successfully"), - readFile: (filePath: string) => - Effect.succeed("data 1\ndata 2\nFAIL\ndata 4"), - }; - }, -}) {} - -// Process a single line -const processLine = (line: string): Effect.Effect => - line === "FAIL" - ? Effect.fail(ProcessError("Failed to process line")) - : Effect.log(`Processed: ${line}`); - -// Create and process the file with proper resource management -const program = Effect.gen(function* () { - yield* Effect.log("=== Stream Resource Management Demo ==="); - yield* Effect.log( - "This demonstrates proper resource cleanup even when errors occur" - ); - - const fileService = yield* FileService; - const { filePath } = yield* fileService.createTempFile(); - - // Use scoped to ensure cleanup happens even on failure - yield* Effect.scoped( - Effect.gen(function* () { - yield* Effect.addFinalizer(() => fileService.cleanup(filePath)); - - const content = yield* fileService.readFile(filePath); - const lines = content.split("\n"); - - // Process each line, continuing even if some fail - for (const line of lines) { - yield* processLine(line).pipe( - Effect.catchAll((error) => - Effect.log(`⚠️ Skipped line due to error: ${error.message}`) - ) - ); - } - - yield* Effect.log( - "✅ Processing completed with proper resource management" - ); - }) - ); -}); - -// Run the program with FileService layer -Effect.runPromise(Effect.provide(program, FileService.Default)).catch( - (error) => { - Effect.runSync(Effect.logError("Unexpected error: " + error)); - } -); -``` - -### Anti-Pattern - -The anti-pattern is to manage resources manually outside the stream's context. This is brittle and almost always leads to resource leaks when errors occur. - -```typescript -import { Effect, Stream } from "effect"; -import { NodeFileSystem } from "@effect/platform-node"; -import * as path from "node:path"; - -const program = Effect.gen(function* () { - const fs = yield* NodeFileSystem; - const filePath = path.join(__dirname, "temp-resource-bad.txt"); - - // 1. Resource acquired manually before the stream - yield* fs.writeFileString(filePath, "data 1\ndata 2"); - const readable = fs.createReadStream(filePath); - yield* Effect.log("Resource acquired manually."); - - const stream = Stream.fromReadable(() => readable).pipe( - Stream.decodeText("utf-8"), - Stream.splitLines, - // This stream will fail, causing the run to reject. - Stream.map(() => { - throw new Error("Something went wrong!"); - }) - ); - - // 2. Stream is executed - yield* Stream.runDrain(stream); - - // 3. This release logic is NEVER reached if the stream fails. - yield* fs.remove(filePath); - yield* Effect.log("Resource released manually. (This will not be logged)"); -}); - -Effect.runPromiseExit(program).then((exit) => { - if (exit._tag === "Failure") { - console.log("\nPipeline failed. The temp file was NOT deleted."); - } -}); -``` - -In this anti-pattern, the `fs.remove` call is unreachable because the `Stream.runDrain` effect fails, causing the `gen` block to terminate immediately. The temporary file is leaked onto the disk. `Stream.acquireRelease` solves this problem entirely. - -### Explanation - -What happens if a pipeline processing a file fails halfway through? In a naive implementation, the file handle might be left open, leading to a resource leak. Over time, these leaks can exhaust system resources and crash your application. - -`Stream.acquireRelease` is Effect's robust solution to this problem. It's built on `Scope`, Effect's fundamental resource-management tool. - -1. **Guaranteed Cleanup**: You provide an `acquire` effect to open the resource and a `release` effect to close it. Effect guarantees that the `release` effect will be called when the stream terminates, for _any_ reason: successful completion, a processing failure, or even external interruption. -2. **Declarative and Co-located**: The logic for a resource's entire lifecycle—acquisition, usage (the stream itself), and release—is defined in one place. This makes the code easier to understand and reason about compared to manual `try/finally` blocks. -3. **Prevents Resource Leaks**: It is the idiomatic way to build truly resilient pipelines that do not leak resources, which is essential for long-running, production-grade applications. -4. **Composability**: The resulting stream is just a normal `Stream`, which can be composed with any other stream operators. - ---- - ---- - -## Manage Shared State Safely with Ref - -**Rule:** Use Ref to manage shared, mutable state concurrently, ensuring atomicity. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const result = yield* program; - yield* Effect.log(`Final counter value: ${result}`); - return result; -}); - -Effect.runPromise(programWithLogging); -``` - ---- - -### Anti-Pattern - -The anti-pattern is using a standard JavaScript variable for shared state. The following example is not guaranteed to produce the correct result. - -```typescript -import { Effect } from "effect"; - -// ❌ WRONG: This is a classic race condition. -const programWithRaceCondition = Effect.gen(function* () { - let count = 0; // A plain, mutable variable - - // An effect that reads, increments, and writes the variable - const increment = Effect.sync(() => { - const current = count; - // Another fiber could run between this read and the write below! - count = current + 1; - }); - - const tasks = Array.from({ length: 1000 }, () => increment); - - yield* Effect.all(tasks, { concurrency: "unbounded" }); - - return count; -}); - -// The result is unpredictable and will likely be less than 1000. -Effect.runPromise(programWithRaceCondition).then(console.log); -``` - -### Explanation - -Directly using a mutable variable (e.g., `let myState = ...`) in a concurrent system is dangerous. Multiple fibers could try to read and write to it at the same time, leading to race conditions and unpredictable results. - -`Ref` solves this by wrapping the state in a fiber-safe container. It's like a synchronized, in-memory cell. All operations on a `Ref` are atomic effects, guaranteeing that updates are applied correctly without being interrupted or interleaved with other updates. This eliminates race conditions and ensures data integrity. - ---- - ---- - -## Manage Shared State Safely with Ref - -**Rule:** Use Ref to safely manage shared, mutable state in concurrent and effectful programs. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Ref } from "effect"; - -// Create a Ref with an initial value -const makeCounter = Ref.make(0); - -// Increment the counter atomically -const increment = makeCounter.pipe( - Effect.flatMap((counter) => Ref.update(counter, (n) => n + 1)) -); - -// Read the current value -const getValue = makeCounter.pipe( - Effect.flatMap((counter) => Ref.get(counter)) -); - -// Use Ref in a workflow -const program = Effect.gen(function* () { - const counter = yield* Ref.make(0); - yield* Ref.update(counter, (n) => n + 1); - const value = yield* Ref.get(counter); - yield* Effect.log(`Counter value: ${value}`); -}); -``` - -**Explanation:** - -- `Ref` is an atomic, mutable reference for effectful and concurrent code. -- All operations are safe, composable, and free of race conditions. -- Use `Ref` for counters, caches, or any shared mutable state. - -### Anti-Pattern - -Using plain variables or objects for shared state in concurrent or async code, which can lead to race conditions, bugs, and unpredictable behavior. - -### Explanation - -Managing shared state with plain variables or objects is unsafe in concurrent or asynchronous code. -`Ref` ensures all updates are atomic and free of race conditions, making your code robust and predictable. - ---- - -## Manually Manage Lifecycles with `Scope` - -**Rule:** Use `Effect.scope` and `Scope.addFinalizer` for fine-grained control over resource cleanup. - -**Skill Level:** advanced - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Console } from "effect"; - -// Mocking a complex file operation -const openFile = (path: string) => - Effect.succeed({ path, handle: Math.random() }).pipe( - Effect.tap((f) => Effect.log(`Opened ${f.path}`)) - ); -const createTempFile = (path: string) => - Effect.succeed({ path: `${path}.tmp`, handle: Math.random() }).pipe( - Effect.tap((f) => Effect.log(`Created temp file ${f.path}`)) - ); -const closeFile = (file: { path: string }) => - Effect.sync(() => Effect.log(`Closed ${file.path}`)); -const deleteFile = (file: { path: string }) => - Effect.sync(() => Effect.log(`Deleted ${file.path}`)); - -// This program acquires two resources (a file and a temp file) -// and ensures both are cleaned up correctly using acquireRelease. -const program = Effect.gen(function* () { - const file = yield* Effect.acquireRelease(openFile("data.csv"), (f) => - closeFile(f) - ); - - const tempFile = yield* Effect.acquireRelease( - createTempFile("data.csv"), - (f) => deleteFile(f) - ); - - yield* Effect.log("...writing data from temp file to main file..."); -}); - -// Run the program with a scope -Effect.runPromise(Effect.scoped(program)); - -/* -Output (note the LIFO cleanup order): -Opened data.csv -Created temp file data.csv.tmp -...writing data from temp file to main file... -Deleted data.csv.tmp -Closed data.csv -*/ -``` - -**Explanation:** -`Effect.scope` creates a new `Scope` and provides it to the `program`. Inside `program`, we access this `Scope` and use `addFinalizer` to register cleanup actions immediately after acquiring each resource. When `Effect.scope` finishes executing `program`, it closes the scope, which in turn executes all registered finalizers in the reverse order of their addition. - -### Anti-Pattern - -Attempting to manage multiple, interdependent resource cleanups using nested `try...finally` blocks. This leads to a "pyramid of doom," is difficult to read, and remains unsafe in the face of interruptions. - -```typescript -// ANTI-PATTERN: Nested, unsafe, and hard to read -async function complexOperation() { - const file = await openFilePromise(); // acquire 1 - try { - const tempFile = await createTempFilePromise(); // acquire 2 - try { - await doWorkPromise(file, tempFile); // use - } finally { - // This block may not run on interruption! - await deleteFilePromise(tempFile); // release 2 - } - } finally { - // This block may also not run on interruption! - await closeFilePromise(file); // release 1 - } -} -``` - -### Explanation - -While `Effect.acquireRelease` and `Layer.scoped` are sufficient for most use cases, sometimes you need more control. This pattern is essential when: - -1. A single logical operation acquires multiple resources that need independent cleanup. -2. You are building a custom, complex `Layer` that orchestrates several dependent resources. -3. You need to understand the fundamental mechanism that powers all of Effect's resource management. - -By interacting with `Scope` directly, you gain precise, imperative-style control over resource cleanup within Effect's declarative, functional framework. Finalizers added to a scope are guaranteed to run in Last-In-First-Out (LIFO) order when the scope is closed. - ---- - -## Mapping and Chaining over Collections with forEach and all - -**Rule:** Use forEach and all to process collections of values with effectful functions, collecting results in a type-safe and composable way. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Either, Option, Stream } from "effect"; - -// Effect: Apply an effectful function to each item in an array -const numbers = [1, 2, 3]; -const effect = Effect.forEach(numbers, (n) => Effect.succeed(n * 2)); -// Effect - -// Effect: Run multiple effects in parallel and collect results -const effects = [Effect.succeed(1), Effect.succeed(2)]; -const allEffect = Effect.all(effects, { concurrency: "unbounded" }); // Effect<[1, 2]> - -// Option: Map over a collection of options and collect only the Some values -const options = [Option.some(1), Option.none(), Option.some(3)]; -const filtered = options.filter(Option.isSome).map((o) => o.value); // [1, 3] - -// Either: Collect all Right values from a collection of Eithers -const eithers = [Either.right(1), Either.left("fail"), Either.right(3)]; -const rights = eithers.filter(Either.isRight); // [Either.Right(1), Either.Right(3)] - -// Stream: Map and flatten a stream of arrays -const stream = Stream.fromIterable([ - [1, 2], - [3, 4], -]).pipe(Stream.flatMap((arr) => Stream.fromIterable(arr))); // Stream -``` - -**Explanation:** -`forEach` and `all` let you process collections in a way that is composable, type-safe, and often parallel. -They handle errors and context automatically, and can be used for batch jobs, parallel requests, or data transformations. - -### Anti-Pattern - -Using manual loops (`for`, `forEach`, etc.) with side effects, or collecting results imperatively, which breaks composability and loses error/context handling. - -### Explanation - -Batch and parallel processing are common in real-world applications. -These combinators let you express "do this for every item" declaratively, without manual loops or imperative control flow, and they preserve error handling and context propagation. - ---- - -## Mapping Errors to Fit Your Domain - -**Rule:** Use Effect.mapError to transform errors and create clean architectural boundaries between layers. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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); -``` - ---- - -### Anti-Pattern - -Allowing low-level, implementation-specific errors to "leak" out of a service's public API. This creates tight coupling between layers. - -```typescript -import { Effect } from "effect"; -import { ConnectionError, QueryError } from "./somewhere"; // From previous example - -// ❌ WRONG: This function's error channel is "leaky". -// It exposes the internal implementation details of the database. -const findUserUnsafely = (): Effect.Effect< - { name: string }, - ConnectionError | QueryError // <-- Leaky abstraction -> => { - // ... logic that calls the database - return Effect.fail(new ConnectionError()); -}; - -// Now, any code that calls `findUserUnsafely` has to know about and handle -// both `ConnectionError` and `QueryError`. If we change the database, -// all of that calling code might have to change too. -``` - -### Explanation - -This pattern is essential for creating clean architectural boundaries and preventing "leaky abstractions." An outer layer of your application (e.g., a `UserService`) should not expose the internal failure details of the layers it depends on (e.g., a `Database` that can fail with `ConnectionError` or `QueryError`). - -By using `Effect.mapError`, the outer layer can define its own, more abstract error type (like `RepositoryError`) and map all the specific, low-level errors into it. This decouples the layers. If you later swap your database implementation, you only need to update the mapping logic within the repository layer; none of the code that _uses_ the repository needs to change. - ---- - ---- - -## Matching on Success and Failure with match - -**Rule:** Use match to pattern match on the result of an Effect, Option, or Either, handling both success and failure cases declaratively. - -**Skill Level:** beginner - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Effect, Option, Either } from "effect"; - -// Effect: Handle both success and failure -const effect = Effect.fail("Oops!").pipe( - Effect.match({ - onFailure: (err) => `Error: ${err}`, - onSuccess: (value) => `Success: ${value}`, - }) -); // Effect - -// Option: Handle Some and None cases -const option = Option.some(42).pipe( - Option.match({ - onNone: () => "No value", - onSome: (n) => `Value: ${n}`, - }) -); // string - -// Either: Handle Left and Right cases -const either = Either.left("fail").pipe( - Either.match({ - onLeft: (err) => `Error: ${err}`, - onRight: (value) => `Value: ${value}`, - }) -); // string -``` - -**Explanation:** - -- `Effect.match` lets you handle both the error and success channels in one place. -- `Option.match` and `Either.match` let you handle all possible cases for these types, making your code exhaustive and safe. - -### Anti-Pattern - -Using nested if/else or switch statements to check for success/failure, or ignoring possible error/none/left cases, which leads to brittle and less readable code. - -### Explanation - -Pattern matching with `match` keeps your code clear and type-safe, ensuring you handle all possible outcomes. -It avoids scattered if/else or switch statements and makes your intent explicit. - ---- - -## Matching Tagged Unions with matchTag and matchTags - -**Rule:** Use matchTag and matchTags to handle specific cases of tagged unions or custom error types in a declarative, type-safe way. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Data, Effect } from "effect"; - -// Define a tagged error type -class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {} -class ValidationError extends Data.TaggedError("ValidationError")<{ - message: string; -}> {} - -type MyError = NotFoundError | ValidationError; - -// Effect: Match on specific error tags -const effect: Effect.Effect = Effect.fail( - new ValidationError({ message: "Invalid input" }) as MyError -).pipe( - Effect.catchTags({ - NotFoundError: () => Effect.succeed("Not found!"), - ValidationError: (err) => - Effect.succeed(`Validation failed: ${err.message}`), - }) -); // Effect -``` - -**Explanation:** - -- `matchTag` lets you branch on the specific tag of a tagged union or custom error type. -- This is safer and more maintainable than using `instanceof` or manual property checks. - -### Anti-Pattern - -Using `instanceof`, manual property checks, or switch statements to distinguish between cases, which is error-prone and less type-safe than declarative pattern matching. - -### Explanation - -Tagged unions (a.k.a. algebraic data types or ADTs) are a powerful way to model domain logic. -Pattern matching on tags lets you handle each case explicitly, making your code robust, maintainable, and exhaustive. - ---- - -## Merge Multiple Streams - -**Rule:** Use merge, concat, or zip to combine multiple streams based on your requirements. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good Example - -```typescript -import { Effect, Stream, Duration, Chunk } from "effect" - -// ============================================ -// 1. Merge - interleave as items arrive -// ============================================ - -const mergeExample = Effect.gen(function* () { - // Two streams producing at different rates - const fast = Stream.fromIterable(["A1", "A2", "A3"]).pipe( - Stream.tap(() => Effect.sleep("100 millis")) - ) - - const slow = Stream.fromIterable(["B1", "B2", "B3"]).pipe( - Stream.tap(() => Effect.sleep("200 millis")) - ) - - // Merge interleaves based on arrival time - const merged = Stream.merge(fast, slow) - - yield* merged.pipe( - Stream.tap((item) => Effect.log(`Received: ${item}`)), - Stream.runDrain - ) - // Output order depends on timing: A1, B1, A2, A3, B2, B3 (approximately) -}) - -// ============================================ -// 2. Merge all - combine many streams -// ============================================ - -const mergeAllExample = Effect.gen(function* () { - const streams = [ - Stream.fromIterable([1, 2, 3]), - Stream.fromIterable([10, 20, 30]), - Stream.fromIterable([100, 200, 300]), - ] - - const merged = Stream.mergeAll(streams, { concurrency: 3 }) - - const results = yield* merged.pipe(Stream.runCollect) - yield* Effect.log(`Merged: ${Chunk.toReadonlyArray(results)}`) -}) - -// ============================================ -// 3. Concat - sequence streams -// ============================================ - -const concatExample = Effect.gen(function* () { - const first = Stream.fromIterable([1, 2, 3]) - const second = Stream.fromIterable([4, 5, 6]) - const third = Stream.fromIterable([7, 8, 9]) - - // Concat waits for each stream to complete - const sequential = Stream.concat(Stream.concat(first, second), third) - - const results = yield* sequential.pipe(Stream.runCollect) - yield* Effect.log(`Concatenated: ${Chunk.toReadonlyArray(results)}`) - // Always: [1, 2, 3, 4, 5, 6, 7, 8, 9] -}) - -// ============================================ -// 4. Zip - pair items from streams -// ============================================ - -const zipExample = Effect.gen(function* () { - const names = Stream.fromIterable(["Alice", "Bob", "Charlie"]) - const ages = Stream.fromIterable([30, 25, 35]) - - // Zip pairs items by position - const zipped = Stream.zip(names, ages) - - yield* zipped.pipe( - Stream.tap(([name, age]) => Effect.log(`${name} is ${age} years old`)), - Stream.runDrain - ) -}) - -// ============================================ -// 5. ZipWith - pair and transform -// ============================================ - -const zipWithExample = Effect.gen(function* () { - const prices = Stream.fromIterable([100, 200, 150]) - const quantities = Stream.fromIterable([2, 1, 3]) - - // Zip and calculate total - const totals = Stream.zipWith(prices, quantities, (price, qty) => ({ - price, - quantity: qty, - total: price * qty, - })) - - yield* totals.pipe( - Stream.tap((item) => Effect.log(`${item.quantity}x @ $${item.price} = $${item.total}`)), - Stream.runDrain - ) -}) - -// ============================================ -// 6. ZipLatest - combine with latest values -// ============================================ - -const zipLatestExample = Effect.gen(function* () { - // Simulate different update rates - const temperature = Stream.fromIterable([20, 21, 22, 23]).pipe( - Stream.tap(() => Effect.sleep("100 millis")) - ) - - const humidity = Stream.fromIterable([50, 55, 60]).pipe( - Stream.tap(() => Effect.sleep("150 millis")) - ) - - // ZipLatest always uses the latest value from each stream - const combined = Stream.zipLatest(temperature, humidity) - - yield* combined.pipe( - Stream.tap(([temp, hum]) => Effect.log(`Temp: ${temp}°C, Humidity: ${hum}%`)), - Stream.runDrain - ) -}) - -// ============================================ -// 7. Practical example: Merge event sources -// ============================================ - -interface Event { - source: string - type: string - data: unknown -} - -const mergeEventSources = Effect.gen(function* () { - // Simulate multiple event sources - const mouseEvents = Stream.fromIterable([ - { source: "mouse", type: "click", data: { x: 100, y: 200 } }, - { source: "mouse", type: "move", data: { x: 150, y: 250 } }, - ] as Event[]) - - const keyboardEvents = Stream.fromIterable([ - { source: "keyboard", type: "keydown", data: { key: "Enter" } }, - { source: "keyboard", type: "keyup", data: { key: "Enter" } }, - ] as Event[]) - - const networkEvents = Stream.fromIterable([ - { source: "network", type: "response", data: { status: 200 } }, - ] as Event[]) - - // Merge all event sources - const allEvents = Stream.mergeAll([mouseEvents, keyboardEvents, networkEvents]) - - yield* allEvents.pipe( - Stream.tap((event) => - Effect.log(`[${event.source}] ${event.type}: ${JSON.stringify(event.data)}`) - ), - Stream.runDrain - ) -}) - -// ============================================ -// 8. Run examples -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Merge Example ===") - yield* mergeExample - - yield* Effect.log("\n=== Concat Example ===") - yield* concatExample - - yield* Effect.log("\n=== Zip Example ===") - yield* zipExample -}) - -Effect.runPromise(program) -``` - -### Explanation - -Merging streams enables: - -1. **Aggregation** - Combine data from multiple sources -2. **Correlation** - Match related data -3. **Multiplexing** - Single consumer for multiple producers -4. **Comparison** - Process streams side by side - ---- - ---- - -## Mocking Dependencies in Tests - -**Rule:** Provide mock service implementations via a test-specific Layer to isolate the unit under test. - -**Skill Level:** intermediate - -**Use Cases:** testing - -### Good 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) => - // Directly return the Effect.log without nesting it in 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)); -``` - ---- - -### Anti-Pattern - -Testing your business logic using the "live" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email). - -```typescript -import { Effect } from "effect"; -import { NotifierLive } from "./somewhere"; -import { EmailClientLive } from "./somewhere"; // The REAL email client - -// ❌ WRONG: This test will try to send a real email. -it("sends a real email", () => - Effect.gen(function* () { - const notifier = yield* Notifier; - yield* notifier.notifyUser(123, "This is a test email!"); - }).pipe( - Effect.provide(NotifierLive), - Effect.provide(EmailClientLive), // Using the live layer makes this an integration test - Effect.runPromise - )); -``` - -### Explanation - -The primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe. - -By providing a mock `Layer` in your test, you replace a real dependency (like an `HttpClient` that makes network calls) with a fake one that returns predictable data. This provides several key benefits: - -- **Determinism:** Your tests always produce the same result, free from the flakiness of network or database connections. -- **Speed:** Tests run instantly without waiting for slow I/O operations. -- **Type Safety:** The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated. -- **Explicitness:** The test setup clearly documents all the dependencies required for the code to run. - ---- - ---- - -## Model Dependencies as Services - -**Rule:** Model dependencies as services. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const value = yield* Effect.provide(program, Random.Default); - yield* Effect.log(`Random value: ${value}`); - return value; -}); - -Effect.runPromise(programWithLogging); -``` - -**Explanation:** -By modeling dependencies as services, you can easily substitute mocked or deterministic implementations for testing, leading to more reliable and predictable tests. - -### Anti-Pattern - -Directly calling external APIs like `fetch` or using impure functions like `Math.random()` within your business logic. This tightly couples your logic to a specific implementation and makes it difficult to test. - -### Explanation - -This pattern is the key to testability. It allows you to provide a `Live` implementation in production and a `Test` implementation (returning mock data) in your tests, making your code decoupled and reliable. - ---- - -## Model Optional Values Safely with Option - -**Rule:** Use Option to explicitly model values that may be absent, avoiding null or undefined. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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 { Effect, 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}!`, - }) - ); - -const program = Effect.gen(function* () { - yield* Effect.log(greeting(1)); // "Welcome, Paul!" - yield* Effect.log(greeting(3)); // "User not found." -}); - -Effect.runPromise(program); -``` - -### Anti-Pattern - -The anti-pattern is returning a nullable type (e.g., User | null or User | undefined). This relies on the discipline of every single caller to perform a null check. Forgetting even one check can introduce a runtime error. - -```typescript -interface User { - id: number; - name: string; -} -const users: User[] = [{ id: 1, name: "Paul" }]; - -// ❌ WRONG: This function's return type is less safe. -const findUserUnsafely = (id: number): User | undefined => { - return users.find((u) => u.id === id); -}; - -const user = findUserUnsafely(3); - -// This will throw "TypeError: Cannot read properties of undefined (reading 'name')" -// because the caller forgot to check if the user exists. -console.log(`User's name is ${user.name}`); -``` - -### Explanation - -Functions that can return a value or `null`/`undefined` are a primary source of runtime errors in TypeScript (`Cannot read properties of null`). - -The `Option` type solves this by making the possibility of an absent value explicit in the type system. A function that returns `Option` cannot be mistaken for a function that returns `User`. The compiler forces you to handle the `None` case before you can access the value inside a `Some`, eliminating an entire class of bugs. - ---- - ---- - -## Model Optional Values Safely with Option - -**Rule:** Use Option to model values that may be present or absent, making absence explicit and type-safe. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Option } from "effect"; - -// Create an Option from a value -const someValue = Option.some(42); // Option -const noValue = Option.none(); // Option - -// Safely convert a nullable value to Option -const fromNullable = Option.fromNullable(Math.random() > 0.5 ? "hello" : null); // Option - -// Pattern match on Option -const result = someValue.pipe( - Option.match({ - onNone: () => "No value", - onSome: (n) => `Value: ${n}`, - }) -); // string - -// Use Option in a workflow -function findUser(id: number): Option.Option<{ id: number; name: string }> { - return id === 1 ? Option.some({ id, name: "Alice" }) : Option.none(); -} -``` - -**Explanation:** - -- `Option.some(value)` represents a present value. -- `Option.none()` represents absence. -- `Option.fromNullable` safely lifts nullable values into Option. -- Pattern matching ensures all cases are handled. - -### Anti-Pattern - -Using `null` or `undefined` to represent absence, or forgetting to handle the "no value" case, which leads to runtime errors and less maintainable code. - -### Explanation - -`Option` makes it impossible to forget to handle the "no value" case. -It improves code safety, readability, and composability, and is a foundation for robust domain modeling. - ---- - -## Model Validated Domain Types with Brand - -**Rule:** Model validated domain types with Brand. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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. - -### Anti-Pattern - -"Primitive obsession"—using raw primitives (`string`, `number`) and performing -validation inside every function that uses them. This is repetitive and -error-prone. - -### Explanation - -This pattern moves validation to the boundaries of your system. Once a value -has been branded, the rest of your application can trust that it is valid, -eliminating repetitive checks. - ---- - -## Modeling Effect Results with Exit - -**Rule:** Use Exit to capture the outcome of an Effect, including success, failure, and defects, for robust error handling and coordination. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Exit } from "effect"; - -// Run an Effect and capture its Exit value -const program = Effect.succeed(42); - -const runAndCapture = Effect.runPromiseExit(program); // Promise> - -// Pattern match on Exit -runAndCapture.then((exit) => { - if (Exit.isSuccess(exit)) { - console.log("Success:", exit.value); - } else if (Exit.isFailure(exit)) { - console.error("Failure:", exit.cause); - } -}); -``` - -**Explanation:** - -- `Exit` captures both success (`Exit.success(value)`) and failure (`Exit.failure(cause)`). -- Use `Exit` for robust error handling, supervision, and coordination of concurrent effects. -- Pattern matching on `Exit` lets you handle all possible outcomes. - -### Anti-Pattern - -Ignoring the outcome of an effect, or only handling success/failure without distinguishing between error types or defects, which can lead to missed errors and less robust code. - -### Explanation - -When running or supervising effects, you often need to know not just if they succeeded or failed, but _how_ they failed (e.g., error vs. defect). -`Exit` provides a complete, type-safe summary of an effect's outcome. - ---- - -## Modeling Tagged Unions with Data.case - -**Rule:** Use Data.case to define tagged unions (ADTs) for modeling domain-specific states and enabling exhaustive pattern matching. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Data } from "effect"; - -// Define a tagged union for a simple state machine -type State = Data.TaggedEnum<{ - Loading: {}; - Success: { data: string }; - Failure: { error: string }; -}>; -const { Loading, Success, Failure } = Data.taggedEnum(); - -// Create instances -const state1: State = Loading(); -const state2: State = Success({ data: "Hello" }); -const state3: State = Failure({ error: "Oops" }); - -// Pattern match on the state -function handleState(state: State): string { - switch (state._tag) { - case "Loading": - return "Loading..."; - case "Success": - return `Data: ${state.data}`; - case "Failure": - return `Error: ${state.error}`; - } -} -``` - -**Explanation:** - -- `Data.case` creates tagged constructors for each state. -- The `_tag` property enables exhaustive pattern matching. -- Use for domain modeling, state machines, and error types. - -### Anti-Pattern - -Using plain objects or enums for domain states, which can lead to illegal states, missed cases, and less type-safe pattern matching. - -### Explanation - -Modeling domain logic with tagged unions ensures that all cases are handled, prevents illegal states, and enables safe, exhaustive pattern matching. -`Data.case` provides a concise, type-safe way to define and use ADTs in your application. - ---- - -## Modeling Validated Domain Types with Brand - -**Rule:** Use Brand to define types like Email, UserId, or PositiveInt, ensuring only valid values can be constructed and used. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good Example - -```typescript -import { Brand } from "effect"; - -// Define a branded type for Email -type Email = string & Brand.Brand<"Email">; - -// Function that only accepts Email, not any string -function sendWelcome(email: Email) { - // ... -} - -// Constructing an Email value (unsafe, see next pattern for validation) -const email = "user@example.com" as Email; - -sendWelcome(email); // OK -// sendWelcome("not-an-email"); // Type error! (commented to allow compilation) -``` - -**Explanation:** - -- `Brand.Branded` creates a new type that is distinct from its base type. -- Only values explicitly branded as `Email` can be used where an `Email` is required. -- This prevents accidental mixing of domain types. - -### Anti-Pattern - -Using plain strings or numbers for domain-specific values (like emails, user IDs, or currency codes), which can lead to accidental misuse and bugs that are hard to catch. - -### Explanation - -Branded types add a layer of type safety, ensuring that values like `Email`, `UserId`, or `PositiveInt` are not confused with plain strings or numbers. -They help you catch bugs at compile time and make your code more self-documenting. - ---- - -## Optional Pattern 1: Handling None and Some Values - -**Rule:** Use Option to represent values that may not exist, replacing null/undefined with type-safe Option that forces explicit handling. - -**Skill Level:** intermediate - -**Use Cases:** value-handling - -### Good Example - -This example demonstrates Option handling patterns. - -```typescript -import { Effect, Option } from "effect"; - -interface User { - id: string; - name: string; - email: string; -} - -interface Profile { - bio: string; - website?: string; - location?: string; -} - -const program = Effect.gen(function* () { - console.log( - `\n[OPTION HANDLING] None/Some values and pattern matching\n` - ); - - // Example 1: Creating Options - console.log(`[1] Creating Option values:\n`); - - const someValue: Option.Option = Option.some("data"); - const noneValue: Option.Option = Option.none(); - - const displayOption = (opt: Option.Option, label: string) => - Effect.gen(function* () { - if (Option.isSome(opt)) { - yield* Effect.log(`${label}: Some(${opt.value})`); - } else { - yield* Effect.log(`${label}: None`); - } - }); - - yield* displayOption(someValue, "someValue"); - yield* displayOption(noneValue, "noneValue"); - - // Example 2: Creating from nullable values - console.log(`\n[2] Converting nullable to Option:\n`); - - const possiblyNull = (shouldExist: boolean): string | null => - shouldExist ? "found" : null; - - const toOption = (value: string | null | undefined): Option.Option => - value ? Option.some(value) : Option.none(); - - const opt1 = toOption(possiblyNull(true)); - const opt2 = toOption(possiblyNull(false)); - - yield* displayOption(opt1, "toOption(found)"); - yield* displayOption(opt2, "toOption(null)"); - - // Example 3: Pattern matching on Option - console.log(`\n[3] Pattern matching with match():\n`); - - const userId: Option.Option = Option.some("user-123"); - - const message = Option.match(userId, { - onSome: (id) => `User ID: ${id}`, - onNone: () => "No user found", - }); - - yield* Effect.log(`[MATCH] ${message}`); - - const emptyUserId: Option.Option = Option.none(); - - const emptyMessage = Option.match(emptyUserId, { - onSome: (id) => `User ID: ${id}`, - onNone: () => "No user found", - }); - - yield* Effect.log(`[MATCH] ${emptyMessage}\n`); - - // Example 4: Transforming with map - console.log(`[4] Transforming values with map():\n`); - - const userCount: Option.Option = Option.some(42); - - const doubled = Option.map(userCount, (count) => count * 2); - - yield* displayOption(doubled, "doubled"); - - // Chaining maps - const email: Option.Option = Option.some("user@example.com"); - - const domain = Option.map(email, (e) => - e.split("@")[1] ?? "unknown" - ); - - yield* displayOption(domain, "email domain"); - - // Example 5: Chaining with flatMap - console.log(`\n[5] Chaining operations with flatMap():\n`); - - const findUser = (id: string): Option.Option => - id === "user-1" - ? Option.some({ id, name: "Alice", email: "alice@example.com" }) - : Option.none(); - - const getProfile = (userId: string): Option.Option => - userId === "user-1" - ? Option.some({ bio: "Developer", website: "alice.dev" }) - : Option.none(); - - const userId2 = Option.some("user-1"); - - // Chained operations: userId -> user -> profile - const profileChain = Option.flatMap(userId2, (id) => - Option.flatMap(findUser(id), (user) => - getProfile(user.id) - ) - ); - - const profileResult = Option.match(profileChain, { - onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`, - onNone: () => "No profile found", - }); - - yield* Effect.log(`[CHAIN] ${profileResult}\n`); - - // Example 6: Fallback values with getOrElse - console.log(`[6] Default values with getOrElse():\n`); - - const optionalStatus: Option.Option = Option.none(); - - const status = Option.getOrElse(optionalStatus, () => "unknown"); - - yield* Effect.log(`[DEFAULT] Status: ${status}`); - - // Real value - const knownStatus: Option.Option = Option.some("active"); - - const realStatus = Option.getOrElse(knownStatus, () => "unknown"); - - yield* Effect.log(`[VALUE] Status: ${realStatus}\n`); - - // Example 7: Filter with predicate - console.log(`[7] Filtering with conditions:\n`); - - const ageOption: Option.Option = Option.some(25); - - const isAdult = Option.filter(ageOption, (age) => age >= 18); - - yield* displayOption(isAdult, "Adult check (25)"); - - const ageOption2: Option.Option = Option.some(15); - - const isAdult2 = Option.filter(ageOption2, (age) => age >= 18); - - yield* displayOption(isAdult2, "Adult check (15)"); - - // Example 8: Multiple Options (all present?) - console.log(`\n[8] Combining multiple Options:\n`); - - const firstName: Option.Option = Option.some("John"); - const lastName: Option.Option = Option.some("Doe"); - const middleName: Option.Option = Option.none(); - - // All three present? - const allPresent = Option.all([firstName, lastName, middleName]); - - yield* displayOption(allPresent, "All present"); - - // Just two - const twoPresent = Option.all([firstName, lastName]); - - yield* displayOption(twoPresent, "Two present"); - - // Example 9: Converting Option to Error - console.log(`\n[9] Converting Option to Result/Error:\n`); - - const optionalConfig: Option.Option<{ apiKey: string }> = Option.none(); - - const configOrError = Option.match(optionalConfig, { - onSome: (config) => config, - onNone: () => { - throw new Error("Configuration not found"); - }, - }); - - // In real code, would catch error - const result = Option.match(optionalConfig, { - onSome: (config) => ({ success: true, value: config }), - onNone: () => ({ success: false, error: "config-not-found" }), - }); - - yield* Effect.log(`[CONVERT] ${JSON.stringify(result)}\n`); - - // Example 10: Option in business logic - console.log(`[10] Practical: Optional user settings:\n`); - - const userSettings: Option.Option<{ - theme: string; - notifications: boolean; - }> = Option.some({ - theme: "dark", - notifications: true, - }); - - const getTheme = Option.map(userSettings, (s) => s.theme); - const theme = Option.getOrElse(getTheme, () => "light"); // Default - - yield* Effect.log(`[SETTING] Theme: ${theme}`); - - // No settings - const noSettings: Option.Option<{ theme: string; notifications: boolean }> = - Option.none(); - - const noTheme = Option.map(noSettings, (s) => s.theme); - const defaultTheme = Option.getOrElse(noTheme, () => "light"); - - yield* Effect.log(`[DEFAULT] Theme: ${defaultTheme}`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Null/undefined causes widespread bugs: - -**Problem 1: Billion-dollar mistake** -- Tony Hoare invented null in ALGOL in 1965 -- Created "billion-dollar mistake" -- 90% of security vulnerabilities involve null handling - -**Problem 2: Undefined behavior** -- `user.profile.name` - any property could be null -- Runtime error: "Cannot read property 'name' of undefined" -- No compile-time warning -- Production crash - -**Problem 3: Silent failures** -- Function returns null on failure -- Caller doesn't check -- Uses null as if it's a value -- Corrupts state downstream - -**Problem 4: Conditional hell** -```javascript -if (user !== null && user.profile !== null && user.profile.name !== null) { - // Do thing -} -``` - -Solutions: - -**Option type**: -- `Some(value)` = value exists -- `None` = value doesn't exist -- Type system forces checking -- No silent null checks possible - -**Pattern matching**: -- `Option.match()` -- Handle both cases explicitly -- Compiler warns if you miss one - -**Chaining**: -- `option.map().flatMap().match()` -- Pipeline of operations -- Null-safe by design - ---- - ---- - -## Optional Pattern 2: Optional Chaining and Composition - -**Rule:** Use Option combinators (map, flatMap, ap) to compose operations that may fail, creating readable and maintainable pipelines. - -**Skill Level:** advanced - -**Use Cases:** value-handling - -### Good Example - -This example demonstrates optional chaining patterns. - -```typescript -import { Effect, Option, pipe } from "effect"; - -interface User { - id: string; - name: string; - email: string; -} - -interface Profile { - bio: string; - website?: string; - avatar?: string; -} - -interface Settings { - theme: "light" | "dark"; - notifications: boolean; - language: string; -} - -const program = Effect.gen(function* () { - console.log(`\n[OPTIONAL CHAINING] Composing Option operations\n`); - - // Example 1: Simple chain with map - console.log(`[1] Chaining transformations with map():\n`); - - const userId: Option.Option = Option.some("user-42"); - - const userDisplayId = Option.map(userId, (id) => `User#${id}`); - - const idMessage = Option.match(userDisplayId, { - onSome: (display) => display, - onNone: () => "No user ID", - }); - - yield* Effect.log(`[CHAIN 1] ${idMessage}`); - - // Chained maps - const email: Option.Option = Option.some("alice@example.com"); - - const emailParts = pipe( - email, - Option.map((e) => e.toLowerCase()), - Option.map((e) => e.split("@")), - Option.map((parts) => parts[0]) // username - ); - - const username = Option.getOrElse(emailParts, () => "unknown"); - - yield* Effect.log(`[USERNAME] ${username}\n`); - - // Example 2: FlatMap for chaining operations that return Option - console.log(`[2] Chaining operations with flatMap():\n`); - - const findUser = (id: string): Option.Option => - id === "user-42" - ? Option.some({ - id, - name: "Alice", - email: "alice@example.com", - }) - : Option.none(); - - const getProfile = (userId: string): Option.Option => - userId === "user-42" - ? Option.some({ - bio: "Software engineer", - website: "alice.dev", - avatar: "https://example.com/avatar.jpg", - }) - : Option.none(); - - const userProfile = pipe( - Option.some("user-42"), - Option.flatMap((id) => findUser(id)), - Option.flatMap((user) => getProfile(user.id)) - ); - - const profileInfo = Option.match(userProfile, { - onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`, - onNone: () => "Profile not found", - }); - - yield* Effect.log(`[PROFILE] ${profileInfo}\n`); - - // Example 3: Complex pipeline - console.log(`[3] Complex pipeline (user → profile → settings → theme):\n`); - - const getSettings = (userId: string): Option.Option => - userId === "user-42" - ? Option.some({ - theme: "dark", - notifications: true, - language: "en", - }) - : Option.none(); - - const userTheme = pipe( - Option.some("user-42"), - Option.flatMap((id) => findUser(id)), - Option.flatMap((user) => getSettings(user.id)), - Option.map((settings) => settings.theme) - ); - - const theme = Option.getOrElse(userTheme, () => "light"); - - yield* Effect.log(`[THEME] ${theme}`); - - // Even if any step is None, result is None - const invalidUserTheme = pipe( - Option.some("invalid-user"), - Option.flatMap((id) => findUser(id)), - Option.flatMap((user) => getSettings(user.id)), - Option.map((settings) => settings.theme) - ); - - const invalidTheme = Option.getOrElse(invalidUserTheme, () => "light"); - - yield* Effect.log(`[DEFAULT THEME] ${invalidTheme}\n`); - - // Example 4: Apply (ap) for combining independent Options - console.log(`[4] Combining values with ap():\n`); - - const firstName: Option.Option = Option.some("John"); - const lastName: Option.Option = Option.some("Doe"); - - // Create a function wrapped in Option - const combineNames = (first: string) => (last: string) => - `${first} ${last}`; - - const fullName = pipe( - Option.some(combineNames), - Option.ap(firstName), - Option.ap(lastName) - ); - - const name = Option.getOrElse(fullName, () => "Unknown"); - - yield* Effect.log(`[COMBINED] ${name}`); - - // If any is None - const noLastName: Option.Option = Option.none(); - - const incompleteName = pipe( - Option.some(combineNames), - Option.ap(firstName), - Option.ap(noLastName) - ); - - const incompleteFull = Option.getOrElse(incompleteName, () => "Incomplete"); - - yield* Effect.log(`[INCOMPLETE] ${incompleteFull}\n`); - - // Example 5: Traverse for mapping over collections - console.log(`[5] Working with collections (traverse):\n`); - - const userIds: string[] = ["user-42", "user-99", "user-1"]; - - // Try to load all users - const allUsers = Option.all( - userIds.map((id) => findUser(id)) - ); - - const usersMessage = Option.match(allUsers, { - onSome: (users) => `Loaded ${users.length} users`, - onNone: () => "Some users not found", - }); - - yield* Effect.log(`[TRAVERSE] ${usersMessage}\n`); - - // Example 6: Or/recovery with multiple options - console.log(`[6] Fallback chains with orElse():\n`); - - const getPrimaryEmail = (): Option.Option => Option.none(); - const getSecondaryEmail = (): Option.Option => - Option.some("backup@example.com"); - const getTertiaryEmail = (): Option.Option => - Option.some("tertiary@example.com"); - - const email1 = pipe( - getPrimaryEmail(), - Option.orElse(() => getSecondaryEmail()), - Option.orElse(() => getTertiaryEmail()) - ); - - const contactEmail = Option.getOrElse(email1, () => "no-email@example.com"); - - yield* Effect.log(`[FALLBACK] Using email: ${contactEmail}\n`); - - // Example 7: Filtering options - console.log(`[7] Filtering with predicates:\n`); - - const age: Option.Option = Option.some(25); - - const canVote = pipe( - age, - Option.filter((a) => a >= 18) - ); - - const voteStatus = Option.match(canVote, { - onSome: () => "Can vote", - onNone: () => "Too young to vote", - }); - - yield* Effect.log(`[FILTER] ${voteStatus}`); - - // Multiple filters in chain - const score: Option.Option = Option.some(85); - - const isAGrade = pipe( - score, - Option.filter((s) => s >= 80), - Option.filter((s) => s < 90) - ); - - const grade = Option.match(isAGrade, { - onSome: () => "Grade A", - onNone: () => "Not in A range", - }); - - yield* Effect.log(`[GRADES] ${grade}\n`); - - // Example 8: Practical: Database query chain - console.log(`[8] Real-world: Database record chain:\n`); - - const getRecord = (id: string): Option.Option<{ data: string; nested: { value: number } }> => - id === "rec-1" - ? Option.some({ - data: "content", - nested: { value: 42 }, - }) - : Option.none(); - - const recordValue = pipe( - Option.some("rec-1"), - Option.flatMap((id) => getRecord(id)), - Option.map((rec) => rec.nested), - Option.map((nested) => nested.value), - Option.map((value) => value * 2) - ); - - const finalValue = Option.getOrElse(recordValue, () => 0); - - yield* Effect.log(`[VALUE] ${finalValue}`); - - // Missing record - const missingValue = pipe( - Option.some("rec-999"), - Option.flatMap((id) => getRecord(id)), - Option.map((rec) => rec.nested), - Option.map((nested) => nested.value), - Option.map((value) => value * 2) - ); - - const defaultValue = Option.getOrElse(missingValue, () => 0); - - yield* Effect.log(`[DEFAULT] ${defaultValue}\n`); - - // Example 9: Conditional chaining - console.log(`[9] Conditional paths:\n`); - - const loadUserWithFallback = (id: string) => - pipe( - findUser(id), - Option.flatMap((user) => - // Only get premium features if user exists - user.name.includes("Alice") - ? Option.some({ ...user, isPremium: true }) - : Option.none() - ), - Option.orElse(() => - // Fallback: return basic user - findUser(id) - ) - ); - - const result1 = loadUserWithFallback("user-42"); - const result2 = loadUserWithFallback("user-99"); - - yield* Effect.log( - `[CONDITIONAL 1] ${Option.match(result1, { onSome: (u) => `${u.name}`, onNone: () => "Not found" })}` - ); - - yield* Effect.log( - `[CONDITIONAL 2] ${Option.match(result2, { onSome: (u) => `${u.name}`, onNone: () => "Not found" })}` - ); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Nested option handling becomes complex: - -**Problem 1: Pyramid of doom** -```typescript -if (user !== null) { - if (user.profile !== null) { - if (user.profile.preferences !== null) { - if (user.profile.preferences.theme !== null) { - // Finally do thing - } - } - } -} -``` - -**Problem 2: Repeated null checks** -- Every step needs its own check -- Code duplicates -- Hard to refactor -- Bugs easy to introduce - -**Problem 3: Logic scattered** -- Transformation logic mixed with null checks -- Hard to understand intent -- Error-prone - -Solutions: - -**Option chaining**: -- `None` flows through automatically -- Transform only if `Some` -- No intermediate checks needed - -**Composition**: -- Combine functions cleanly -- Separate concerns -- Reusable pieces - -**Fallbacks**: -- `orElse()` for recovery -- Chain multiple alternatives -- Graceful degradation - ---- - ---- - -## Organize Layers into Composable Modules - -**Rule:** Organize services into modular Layers that are composed hierarchically to manage complexity in large applications. - -**Skill Level:** advanced - -**Use Cases:** testing - -### Good Example - -This example shows a `BaseLayer` with a `Logger`, a `UserModule` that uses the `Logger`, and a final `AppLayer` that wires them together. - -### 1. The Base Infrastructure Layer - -```typescript -// src/core/Logger.ts -import { Effect } from "effect"; - -export class Logger extends Effect.Service()("App/Core/Logger", { - sync: () => ({ - log: (msg: string) => Effect.log(`[LOG] ${msg}`), - }), -}) {} - -// src/features/User/UserRepository.ts -export class UserRepository extends Effect.Service()( - "App/User/UserRepository", - { - // Define implementation that uses Logger - effect: Effect.gen(function* () { - const logger = yield* Logger; - return { - findById: (id: number) => - Effect.gen(function* () { - yield* logger.log(`Finding user ${id}`); - return { id, name: `User ${id}` }; - }), - }; - }), - // Declare Logger dependency - dependencies: [Logger.Default], - } -) {} - -// Example usage -const program = Effect.gen(function* () { - const repo = yield* UserRepository; - const user = yield* repo.findById(1); - return user; -}); - -// Run with default implementations -Effect.runPromise(Effect.provide(program, UserRepository.Default)); - -const programWithLogging = Effect.gen(function* () { - const result = yield* program; - yield* Effect.log(`Program result: ${JSON.stringify(result)}`); - return result; -}); - -Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default)); -``` - -### 2. The Feature Module Layer - -```typescript -// src/core/Logger.ts -import { Effect } from "effect"; - -export class Logger extends Effect.Service()("App/Core/Logger", { - sync: () => ({ - log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)), - }), -}) {} - -// src/features/User/UserRepository.ts -export class UserRepository extends Effect.Service()( - "App/User/UserRepository", - { - // Define implementation that uses Logger - effect: Effect.gen(function* () { - const logger = yield* Logger; - return { - findById: (id: number) => - Effect.gen(function* () { - yield* logger.log(`Finding user ${id}`); - return { id, name: `User ${id}` }; - }), - }; - }), - // Declare Logger dependency - dependencies: [Logger.Default], - } -) {} - -// Example usage -const program = Effect.gen(function* () { - const repo = yield* UserRepository; - const user = yield* repo.findById(1); - return user; -}); - -// Run with default implementations -Effect.runPromise(Effect.provide(program, UserRepository.Default)).then( - console.log -); -``` - -### 3. The Final Application Composition - -```typescript -// src/layers.ts -import { Layer } from "effect"; -import { BaseLayer } from "./core"; -import { UserModuleLive } from "./features/User"; -// import { ProductModuleLive } from "./features/Product"; - -const AllModules = Layer.mergeAll(UserModuleLive /*, ProductModuleLive */); - -// Provide the BaseLayer to all modules at once, creating a self-contained AppLayer. -export const AppLayer = Layer.provide(AllModules, BaseLayer); -``` - ---- - -### Anti-Pattern - -A flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage. - -```typescript -// ❌ This file becomes huge and hard to navigate in a large project. -const AppLayer = Layer.mergeAll( - LoggerLive, - ConfigLive, - DatabaseLive, - TracerLive, - UserServiceLive, - UserRepositoryLive, - ProductServiceLive, - ProductRepositoryLive, - BillingServiceLive - // ...and 50 other services -); -``` - -### Explanation - -As an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure. - -This approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked `BaseLayer`. - ---- - ---- - -## Parse and Validate Data with Schema.decode - -**Rule:** Parse and validate data with Schema.decode. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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. - -### Anti-Pattern - -Using `Schema.parse(schema)(input)`, as it throws an exception. This forces -you to use `try/catch` blocks, which breaks the composability of Effect. - -### Explanation - -Unlike the older `Schema.parse` which throws, `Schema.decode` is fully -integrated into the Effect ecosystem, allowing you to handle validation -failures gracefully with operators like `Effect.catchTag`. - ---- - -## Parse JSON Responses Safely - -**Rule:** Always validate HTTP responses with Schema to catch API changes at runtime. - -**Skill Level:** beginner - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Console } from "effect" -import { Schema } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" -import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" - -// ============================================ -// 1. Define response schemas -// ============================================ - -const PostSchema = Schema.Struct({ - id: Schema.Number, - title: Schema.String, - body: Schema.String, - userId: Schema.Number, -}) - -type Post = Schema.Schema.Type - -const PostArraySchema = Schema.Array(PostSchema) - -// ============================================ -// 2. Fetch and validate single item -// ============================================ - -const getPost = (id: number) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const response = yield* client.get( - `https://jsonplaceholder.typicode.com/posts/${id}` - ) - const json = yield* HttpClientResponse.json(response) - - // Validate against schema - fails if data doesn't match - const post = yield* Schema.decodeUnknown(PostSchema)(json) - - return post - }) - -// ============================================ -// 3. Fetch and validate array -// ============================================ - -const getPosts = Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const response = yield* client.get( - "https://jsonplaceholder.typicode.com/posts" - ) - const json = yield* HttpClientResponse.json(response) - - // Validate array of posts - const posts = yield* Schema.decodeUnknown(PostArraySchema)(json) - - return posts -}) - -// ============================================ -// 4. Handle validation errors -// ============================================ - -const safeGetPost = (id: number) => - getPost(id).pipe( - Effect.catchTag("ParseError", (error) => - Effect.gen(function* () { - yield* Console.error(`Invalid response format: ${error.message}`) - // Return a default or fail differently - return yield* Effect.fail(new Error(`Post ${id} has invalid format`)) - }) - ) - ) - -// ============================================ -// 5. Schema with optional fields -// ============================================ - -const UserSchema = Schema.Struct({ - id: Schema.Number, - name: Schema.String, - email: Schema.String, - phone: Schema.optional(Schema.String), // May not exist - website: Schema.optional(Schema.String), - company: Schema.optional( - Schema.Struct({ - name: Schema.String, - catchPhrase: Schema.optional(Schema.String), - }) - ), -}) - -const getUser = (id: number) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const response = yield* client.get( - `https://jsonplaceholder.typicode.com/users/${id}` - ) - const json = yield* HttpClientResponse.json(response) - - return yield* Schema.decodeUnknown(UserSchema)(json) - }) - -// ============================================ -// 6. Run examples -// ============================================ - -const program = Effect.gen(function* () { - yield* Console.log("=== Validated Single Post ===") - const post = yield* getPost(1) - yield* Console.log(`Title: ${post.title}`) - - yield* Console.log("\n=== Validated Posts Array ===") - const posts = yield* getPosts - yield* Console.log(`Fetched ${posts.length} posts`) - - yield* Console.log("\n=== User with Optional Fields ===") - const user = yield* getUser(1) - yield* Console.log(`User: ${user.name}`) - yield* Console.log(`Company: ${user.company?.name ?? "N/A"}`) -}) - -program.pipe( - Effect.provide(NodeHttpClient.layer), - NodeRuntime.runMain -) -``` - -### Explanation - -APIs can change without warning: - -1. **Fields disappear** - Backend removes a field -2. **Types change** - String becomes number -3. **Nulls appear** - Required field becomes optional -4. **New fields** - Extra data you didn't expect - -Schema validation catches these issues immediately. - ---- - ---- - -## Pattern Match on Option and Either - -**Rule:** Use Option.match() and Either.match() for declarative pattern matching on optional and error-prone values - -**Skill Level:** beginner - -**Use Cases:** error-management - -### Good Example - -### Basic Option Matching - -```typescript -import { Option } from "effect"; - -const getUserName = (id: number): Option.Option => { - return id === 1 ? Option.some("Alice") : Option.none(); -}; - -// Using .match() for declarative pattern matching -const displayUser = (id: number): string => - getUserName(id).pipe( - Option.match({ - onNone: () => "Guest User", - onSome: (name) => `Hello, ${name}!`, - }) - ); - -console.log(displayUser(1)); // "Hello, Alice!" -console.log(displayUser(999)); // "Guest User" -``` - -### Basic Either Matching - -```typescript -import { Either } from "effect"; - -const validateAge = (age: number): Either.Either => { - return age >= 18 - ? Either.right(age) - : Either.left("Must be 18 or older"); -}; - -// Using .match() for error handling -const processAge = (age: number): string => - validateAge(age).pipe( - Either.match({ - onLeft: (error) => `Validation failed: ${error}`, - onRight: (validAge) => `Age ${validAge} is valid`, - }) - ); - -console.log(processAge(25)); // "Age 25 is valid" -console.log(processAge(15)); // "Validation failed: Must be 18 or older" -``` - -### Advanced: Nested Matching - -When dealing with nested Option and Either, use nested `.match()` calls: - -```typescript -import { Option, Either } from "effect"; - -interface UserProfile { - name: string; - age: number; -} - -const getUserProfile = ( - id: number -): Option.Option> => { - if (id === 0) return Option.none(); // User not found - if (id === 1) return Option.some(Either.left("Profile incomplete")); - return Option.some(Either.right({ name: "Bob", age: 25 })); -}; - -// Nested matching - first on Option, then on Either -const displayProfile = (id: number): string => - getUserProfile(id).pipe( - Option.match({ - onNone: () => "User not found", - onSome: (result) => - result.pipe( - Either.match({ - onLeft: (error) => `Error: ${error}`, - onRight: (profile) => `${profile.name} (${profile.age})`, - }) - ), - }) - ); - -console.log(displayProfile(0)); // "User not found" -console.log(displayProfile(1)); // "Error: Profile incomplete" -console.log(displayProfile(2)); // "Bob (25)" -``` - -### Anti-Pattern - -Avoid manual conditional checks and nested ternaries: - -```typescript -// ❌ ANTI-PATTERN: Imperative checks with isSome/isLeft -const name = getUserName(1); -let result: string; -if (Option.isSome(name)) { - result = `Hello, ${name.value}!`; -} else { - result = "Guest User"; -} - -// ❌ ANTI-PATTERN: Nested ternaries -const ageResult = validateAge(25); -const message = ageResult.pipe( - Either.match({ - onLeft: () => "Invalid", - onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink", - }) -); - -// ❌ ANTI-PATTERN: Chained if-else instead of match -function processValue(value: Option.Option): string { - if (Option.isSome(value)) { - if (value.value > 0) { - return "Positive"; - } else if (value.value < 0) { - return "Negative"; - } else { - return "Zero"; - } - } - return "No value"; -} -``` - -Why these are worse: -- **Less readable**: The intent is hidden in imperative logic -- **Error-prone**: Easy to forget cases or introduce bugs -- **Mutable state**: Often requires intermediate variables -- **Less composable**: Harder to pipe and combine operations - -### Explanation - -The `.match()` combinator is superior to manual checks (`isSome()`, `isLeft()`) because: - -1. **Declarative**: Expresses intent clearly - "match on these cases" -2. **Type-safe**: TypeScript ensures all cases are handled -3. **Exhaustive**: You can't accidentally miss a case -4. **Composable**: Works naturally with `.pipe()` for chaining operations -5. **Readable**: The structure mirrors the data type itself - -Without `.match()`, you'd need imperative conditionals, which are harder to read and easier to get wrong. - ---- - -## Platform Pattern 1: Execute Shell Commands - -**Rule:** Use Command to spawn and manage external processes, capturing output and handling exit codes reliably with proper error handling. - -**Skill Level:** intermediate - -**Use Cases:** platform - -### Good Example - -This example demonstrates executing commands and handling their output. - -```typescript -import { Command, Effect, Chunk } from "@effect/platform"; - -// Simple command execution -const program = Effect.gen(function* () { - console.log(`\n[COMMAND] Executing shell commands\n`); - - // Example 1: List files - console.log(`[1] List files in current directory:\n`); - - const lsResult = yield* Command.make("ls", ["-la"]).pipe( - Command.string - ); - - console.log(lsResult); - - // Example 2: Get current date - console.log(`\n[2] Get current date:\n`); - - const dateResult = yield* Command.make("date", ["+%Y-%m-%d %H:%M:%S"]).pipe( - Command.string - ); - - console.log(`Current date: ${dateResult.trim()}`); - - // Example 3: Capture exit code - console.log(`\n[3] Check if file exists:\n`); - - const fileCheckCmd = yield* Command.make("test", [ - "-f", - "/etc/passwd", - ]).pipe( - Command.exitCode, - Effect.either - ); - - if (fileCheckCmd._tag === "Right") { - console.log(`✓ File exists (exit code: 0)`); - } else { - console.log(`✗ File not found (exit code: ${fileCheckCmd.left})`); - } - - // Example 4: Execute with custom working directory - console.log(`\n[4] List TypeScript files:\n`); - - const findResult = yield* Command.make("find", [ - ".", - "-name", - "*.ts", - "-type", - "f", - ]).pipe( - Command.lines - ); - - const tsFiles = Chunk.take(findResult, 5); // First 5 - - Chunk.forEach(tsFiles, (file) => { - console.log(` - ${file}`); - }); - - if (Chunk.size(findResult) > 5) { - console.log(` ... and ${Chunk.size(findResult) - 5} more`); - } - - // Example 5: Handle command failure - console.log(`\n[5] Handle command failure gracefully:\n`); - - const failResult = yield* Command.make("false").pipe( - Command.exitCode, - Effect.catchAll((error) => - Effect.succeed(-1) // Return -1 for any error - ) - ); - - console.log(`Exit code: ${failResult}`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Shell integration without proper handling causes issues: - -- **Unhandled errors**: Non-zero exit codes lost -- **Deadlocks**: Stdout buffer fills if not drained -- **Resource leaks**: Processes left running -- **Output loss**: stderr ignored -- **Race conditions**: Unsafe concurrent execution - -Command enables: - -- **Type-safe execution**: Success/failure handled in Effect -- **Output capture**: Both stdout and stderr available -- **Resource cleanup**: Automatic process termination -- **Exit code handling**: Explicit error mapping - -Real-world example: Build pipeline -- **Direct**: Process spawned, output mixed with app logs, exit code ignored -- **With Command**: Output captured, exit code checked, errors propagated - ---- - ---- - -## Platform Pattern 2: Filesystem Operations - -**Rule:** Use FileSystem module for safe, resource-managed file operations with proper error handling and cleanup. - -**Skill Level:** beginner - -**Use Cases:** platform - -### Good Example - -This example demonstrates reading, writing, and manipulating files. - -```typescript -import { FileSystem, Effect, Stream } from "@effect/platform"; -import * as fs from "fs/promises"; - -const program = Effect.gen(function* () { - console.log(`\n[FILESYSTEM] Demonstrating file operations\n`); - - // Example 1: Write a file - console.log(`[1] Writing file:\n`); - - const content = `Hello, Effect-TS!\nThis is a test file.\nCreated at ${new Date().toISOString()}`; - - yield* FileSystem.writeFileUtf8("test.txt", content); - - yield* Effect.log(`✓ File written: test.txt`); - - // Example 2: Read the file - console.log(`\n[2] Reading file:\n`); - - const readContent = yield* FileSystem.readFileUtf8("test.txt"); - - console.log(readContent); - - // Example 3: Get file stats - console.log(`\n[3] File stats:\n`); - - const stats = yield* FileSystem.stat("test.txt").pipe( - Effect.flatMap((stat) => - Effect.succeed({ - size: stat.size, - isFile: stat.isFile(), - modified: stat.mtimeMs, - }) - ) - ); - - console.log(` Size: ${stats.size} bytes`); - console.log(` Is file: ${stats.isFile}`); - console.log(` Modified: ${new Date(stats.modified).toISOString()}`); - - // Example 4: Create directory and write multiple files - console.log(`\n[4] Creating directory and files:\n`); - - yield* FileSystem.mkdir("test-dir"); - - yield* Effect.all( - Array.from({ length: 3 }, (_, i) => - FileSystem.writeFileUtf8( - `test-dir/file-${i + 1}.txt`, - `Content of file ${i + 1}` - ) - ) - ); - - yield* Effect.log(`✓ Created directory with 3 files`); - - // Example 5: List directory contents - console.log(`\n[5] Listing directory:\n`); - - const entries = yield* FileSystem.readDirectory("test-dir"); - - entries.forEach((entry) => { - console.log(` - ${entry}`); - }); - - // Example 6: Append to file - console.log(`\n[6] Appending to file:\n`); - - const appendContent = `\nAppended line at ${new Date().toISOString()}`; - - yield* FileSystem.appendFileUtf8("test.txt", appendContent); - - const finalContent = yield* FileSystem.readFileUtf8("test.txt"); - - console.log(`File now has ${finalContent.split("\n").length} lines`); - - // Example 7: Clean up - console.log(`\n[7] Cleaning up:\n`); - - yield* Effect.all( - Array.from({ length: 3 }, (_, i) => - FileSystem.remove(`test-dir/file-${i + 1}.txt`) - ) - ); - - yield* FileSystem.remove("test-dir"); - yield* FileSystem.remove("test.txt"); - - yield* Effect.log(`✓ Cleanup complete`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Direct file operations without FileSystem create issues: - -- **Resource leaks**: Files not closed on errors -- **No error context**: Missing file names in errors -- **Blocking**: No async/await integration -- **Cross-platform**: Path handling differences - -FileSystem enables: - -- **Resource safety**: Automatic cleanup -- **Error context**: Full error messages -- **Async integration**: Effect-native -- **Cross-platform**: Handles path separators - -Real-world example: Process log files -- **Direct**: Open file, read, close, handle exceptions manually -- **With FileSystem**: `FileSystem.read(path).pipe(...)` - ---- - ---- - -## Platform Pattern 3: Persistent Key-Value Storage - -**Rule:** Use KeyValueStore for simple persistent storage of key-value pairs, enabling lightweight caching and session management. - -**Skill Level:** intermediate - -**Use Cases:** platform - -### Good Example - -This example demonstrates storing and retrieving persistent data. - -```typescript -import { KeyValueStore, Effect } from "@effect/platform"; - -interface UserSession { - readonly userId: string; - readonly token: string; - readonly expiresAt: number; -} - -const program = Effect.gen(function* () { - console.log(`\n[KEYVALUESTORE] Persistent storage example\n`); - - const store = yield* KeyValueStore.KeyValueStore; - - // Example 1: Store session data - console.log(`[1] Storing session:\n`); - - const session: UserSession = { - userId: "user-123", - token: "token-abc-def", - expiresAt: Date.now() + 3600000, // 1 hour - }; - - yield* store.set("session:user-123", JSON.stringify(session)); - - yield* Effect.log(`✓ Session stored`); - - // Example 2: Retrieve stored data - console.log(`\n[2] Retrieving session:\n`); - - const stored = yield* store.get("session:user-123"); - - if (stored._tag === "Some") { - const retrievedSession = JSON.parse(stored.value) as UserSession; - - console.log(` User ID: ${retrievedSession.userId}`); - console.log(` Token: ${retrievedSession.token}`); - console.log( - ` Expires: ${new Date(retrievedSession.expiresAt).toISOString()}` - ); - } - - // Example 3: Check if key exists - console.log(`\n[3] Checking keys:\n`); - - const hasSession = yield* store.has("session:user-123"); - const hasOther = yield* store.has("session:user-999"); - - console.log(` Has session:user-123: ${hasSession}`); - console.log(` Has session:user-999: ${hasOther}`); - - // Example 4: Store multiple cache entries - console.log(`\n[4] Caching API responses:\n`); - - const apiResponses = [ - { endpoint: "/api/users", data: [{ id: 1, name: "Alice" }] }, - { endpoint: "/api/posts", data: [{ id: 1, title: "First Post" }] }, - { endpoint: "/api/comments", data: [] }, - ]; - - yield* Effect.all( - apiResponses.map((item) => - store.set( - `cache:${item.endpoint}`, - JSON.stringify(item.data) - ) - ) - ); - - yield* Effect.log(`✓ Cached ${apiResponses.length} endpoints`); - - // Example 5: Retrieve cache with expiration - console.log(`\n[5] Checking cached data:\n`); - - for (const item of apiResponses) { - const cached = yield* store.get(`cache:${item.endpoint}`); - - if (cached._tag === "Some") { - const data = JSON.parse(cached.value); - - console.log( - ` ${item.endpoint}: ${Array.isArray(data) ? data.length : 1} items` - ); - } - } - - // Example 6: Remove specific entry - console.log(`\n[6] Removing entry:\n`); - - yield* store.remove("cache:/api/comments"); - - const removed = yield* store.has("cache:/api/comments"); - - console.log(` Exists after removal: ${removed}`); - - // Example 7: Iterate and count entries - console.log(`\n[7] Counting entries:\n`); - - const allKeys = yield* store.entries.pipe( - Effect.map((entries) => entries.length) - ); - - console.log(` Total entries: ${allKeys}`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Without persistent storage, transient data is lost: - -- **Session data**: Lost on restart -- **Caches**: Rebuilt from scratch -- **Configuration**: Hardcoded or file-based -- **State**: Scattered across code - -KeyValueStore enables: - -- **Transparent persistence**: Automatic backend handling -- **Simple API**: Key-value abstraction -- **Pluggable backends**: Memory, filesystem, database -- **Effect integration**: Type-safe, composable - -Real-world example: Caching API responses -- **Direct**: Cache in memory Map (lost on restart) -- **With KeyValueStore**: Persistent across restarts - ---- - ---- - -## Platform Pattern 4: Interactive Terminal I/O - -**Rule:** Use Terminal for user input/output in CLI applications, providing proper buffering and cross-platform character encoding. - -**Skill Level:** beginner - -**Use Cases:** platform - -### Good Example - -This example demonstrates building an interactive CLI application. - -```typescript -import { Terminal, Effect } from "@effect/platform"; - -interface UserInput { - readonly name: string; - readonly email: string; - readonly age: number; -} - -const program = Effect.gen(function* () { - console.log(`\n[INTERACTIVE CLI] User Information Form\n`); - - // Example 1: Simple prompts - yield* Terminal.writeLine(`=== User Setup ===`); - yield* Terminal.writeLine(``); - - yield* Terminal.write(`What is your name? `); - const name = yield* Terminal.readLine(); - - yield* Terminal.write(`What is your email? `); - const email = yield* Terminal.readLine(); - - yield* Terminal.write(`What is your age? `); - const ageStr = yield* Terminal.readLine(); - - const age = parseInt(ageStr); - - // Example 2: Display collected information - yield* Terminal.writeLine(``); - yield* Terminal.writeLine(`=== Summary ===`); - yield* Terminal.writeLine(`Name: ${name}`); - yield* Terminal.writeLine(`Email: ${email}`); - yield* Terminal.writeLine(`Age: ${age}`); - - // Example 3: Confirmation - yield* Terminal.writeLine(``); - yield* Terminal.write(`Confirm information? (yes/no) `); - const confirm = yield* Terminal.readLine(); - - if (confirm.toLowerCase() === "yes") { - yield* Terminal.writeLine(`✓ Information saved`); - } else { - yield* Terminal.writeLine(`✗ Cancelled`); - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Direct stdin/stdout causes issues: - -- **No buffering**: Interleaved output in concurrent context -- **Encoding issues**: Special characters corrupted -- **Password echo**: Security vulnerability -- **No type safety**: String manipulation error-prone - -Terminal enables: - -- **Buffered I/O**: Safe concurrent output -- **Encoding handling**: UTF-8 and special chars -- **Password input**: No echo mode -- **Structured interaction**: Prompts and validation - -Real-world example: CLI setup wizard -- **Direct**: console.log mixed with readline, no error handling -- **With Terminal**: Structured input, validation, formatted output - ---- - ---- - -## Platform Pattern 5: Cross-Platform Path Manipulation - -**Rule:** Use Effect's platform-aware path utilities to handle separators, absolute/relative paths, and environment variables consistently. - -**Skill Level:** intermediate - -**Use Cases:** platform - -### Good Example - -This example demonstrates cross-platform path manipulation. - -```typescript -import { Effect, FileSystem } from "@effect/platform"; -import * as Path from "node:path"; -import * as OS from "node:os"; - -interface PathOperation { - readonly input: string; - readonly description: string; -} - -// Platform info -const getPlatformInfo = () => - Effect.gen(function* () { - const platform = process.platform; - const separator = Path.sep; - const delimiter = Path.delimiter; - const homeDir = OS.homedir(); - - yield* Effect.log( - `[PLATFORM] OS: ${platform}, Separator: "${separator}", Home: ${homeDir}` - ); - - return { platform, separator, delimiter, homeDir }; - }); - -const program = Effect.gen(function* () { - console.log(`\n[PATH MANIPULATION] Cross-platform path operations\n`); - - const platformInfo = yield* getPlatformInfo(); - - // Example 1: Path joining (handles separators) - console.log(`\n[1] Joining paths (handles separators automatically):\n`); - - const segments = ["data", "reports", "2024"]; - - const joinedPath = Path.join(...segments); - - yield* Effect.log(`[JOIN] Input: ${segments.join(" + ")}`); - yield* Effect.log(`[JOIN] Output: ${joinedPath}`); - - // Example 2: Resolving to absolute paths - console.log(`\n[2] Resolving relative → absolute:\n`); - - const relativePath = "./config/settings.json"; - - const absolutePath = Path.resolve(relativePath); - - yield* Effect.log(`[RESOLVE] Relative: ${relativePath}`); - yield* Effect.log(`[RESOLVE] Absolute: ${absolutePath}`); - - // Example 3: Path parsing - console.log(`\n[3] Parsing path components:\n`); - - const filePath = "/home/user/documents/report.pdf"; - - const parsed = Path.parse(filePath); - - yield* Effect.log(`[PARSE] Input: ${filePath}`); - yield* Effect.log(` root: ${parsed.root}`); - yield* Effect.log(` dir: ${parsed.dir}`); - yield* Effect.log(` base: ${parsed.base}`); - yield* Effect.log(` name: ${parsed.name}`); - yield* Effect.log(` ext: ${parsed.ext}`); - - // Example 4: Environment variable expansion - console.log(`\n[4] Environment variable expansion:\n`); - - const expandPath = (pathStr: string): string => { - let result = pathStr; - - // Expand common variables - result = result.replace("$HOME", OS.homedir()); - result = result.replace("~", OS.homedir()); - result = result.replace("$USER", process.env.USER || "user"); - result = result.replace("$PWD", process.cwd()); - - // Handle Windows-style env vars - result = result.replace(/%USERPROFILE%/g, OS.homedir()); - result = result.replace(/%USERNAME%/g, process.env.USERNAME || "user"); - result = result.replace(/%TEMP%/g, OS.tmpdir()); - - return result; - }; - - const envPaths = [ - "$HOME/myapp/data", - "~/documents/file.txt", - "$PWD/config", - "/var/log/app.log", - ]; - - for (const envPath of envPaths) { - const expanded = expandPath(envPath); - - yield* Effect.log( - `[EXPAND] ${envPath} → ${expanded}` - ); - } - - // Example 5: Path normalization (remove redundant separators) - console.log(`\n[5] Path normalization:\n`); - - const messyPaths = [ - "/home//user///documents", - "C:\\Users\\\\documents\\\\file.txt", - "./config/../config/./settings", - "../data/../../root", - ]; - - for (const messy of messyPaths) { - const normalized = Path.normalize(messy); - - yield* Effect.log( - `[NORMALIZE] ${messy}` - ); - yield* Effect.log( - `[NORMALIZE] → ${normalized}` - ); - } - - // Example 6: Safe path construction with base directory - console.log(`\n[6] Safe path construction (path traversal prevention):\n`); - - const baseDir = "/var/app/data"; - - const safeJoin = (base: string, userPath: string): Result => { - // Reject absolute paths from untrusted input - if (Path.isAbsolute(userPath)) { - return { success: false, reason: "Absolute paths not allowed" }; - } - - // Reject paths with .. - if (userPath.includes("..")) { - return { success: false, reason: "Path traversal attempt detected" }; - } - - // Resolve and verify within base - const fullPath = Path.resolve(base, userPath); - - if (!fullPath.startsWith(base)) { - return { success: false, reason: "Path escapes base directory" }; - } - - return { success: true, path: fullPath }; - }; - - interface Result { - success: boolean; - reason?: string; - path?: T; - } - - const testPaths = [ - "reports/2024.json", - "/etc/passwd", - "../../../root", - "data/file.txt", - ]; - - for (const test of testPaths) { - const result = safeJoin(baseDir, test); - - if (result.success) { - yield* Effect.log(`[SAFE] ✓ ${test} → ${result.path}`); - } else { - yield* Effect.log(`[SAFE] ✗ ${test} (${result.reason})`); - } - } - - // Example 7: Relative path calculation - console.log(`\n[7] Computing relative paths:\n`); - - const fromDir = "/home/user/projects/myapp"; - const toPath = "/home/user/data/config.json"; - - const relativePath2 = Path.relative(fromDir, toPath); - - yield* Effect.log(`[RELATIVE] From: ${fromDir}`); - yield* Effect.log(`[RELATIVE] To: ${toPath}`); - yield* Effect.log(`[RELATIVE] Relative: ${relativePath2}`); - - // Example 8: Common path patterns - console.log(`\n[8] Common patterns:\n`); - - // Get file extension - const fileName = "document.tar.gz"; - const ext = Path.extname(fileName); - const baseName = Path.basename(fileName); - const dirName = Path.dirname("/home/user/file.txt"); - - yield* Effect.log(`[PATTERNS] File: ${fileName}`); - yield* Effect.log(` basename: ${baseName}`); - yield* Effect.log(` dirname: ${dirName}`); - yield* Effect.log(` extname: ${ext}`); - - // Example 9: Path segments array - console.log(`\n[9] Path segments:\n`); - - const segmentPath = "/home/user/documents/report.pdf"; - - const segments2 = segmentPath.split(Path.sep).filter((s) => s); - - yield* Effect.log(`[SEGMENTS] ${segmentPath}`); - yield* Effect.log(`[SEGMENTS] → [${segments2.map((s) => `"${s}"`).join(", ")}]`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -String-based path handling causes problems: - -**Problem 1: Platform inconsistency** -- Write path: `"C:\data\file.txt"` (Windows) -- Ship to Linux, gets interpreted as literal "C:\data\file.txt" -- File not found errors, production outage - -**Problem 2: Path traversal attacks** -- User supplies path: `"../../../../etc/passwd"` -- No validation → reads sensitive files -- Security vulnerability - -**Problem 3: Environment variable expansion** -- User's config: `"$HOME/myapp/data"` -- Without expansion: literal `$HOME` in path -- Can't find files - -**Problem 4: Symlink resolution** -- File at `/etc/ssl/certs/ca-bundle.crt` (symlink) -- Real file at `/usr/share/ca-certificates/ca-bundle.crt` -- Both point to same file, but string equality fails - -Solutions: - -**Platform-aware API**: -- `path.join()` handles separators -- `path.resolve()` creates absolute paths -- `path.parse()` components -- Auto-handles platform differences - -**Variable expansion**: -- `$HOME`, `~` → user home -- `$USER` → username -- `$PWD` → current directory - -**Validation**: -- Reject paths with `..` -- Reject absolute paths from untrusted input -- Contain paths within base directory - ---- - ---- - -## Platform Pattern 6: Advanced FileSystem Operations - -**Rule:** Use advanced file system patterns to implement efficient, reliable file operations with proper error handling and resource cleanup. - -**Skill Level:** advanced - -**Use Cases:** platform - -### Good Example - -This example demonstrates advanced file system patterns. - -```typescript -import { Effect, Stream, Ref, FileSystem } from "@effect/platform"; -import * as Path from "node:path"; -import * as FS from "node:fs"; -import * as PromiseFS from "node:fs/promises"; - -const program = Effect.gen(function* () { - console.log(`\n[ADVANCED FILESYSTEM] Complex file operations\n`); - - // Example 1: Atomic file write with temporary file - console.log(`[1] Atomic write (crash-safe):\n`); - - const atomicWrite = ( - filePath: string, - content: string - ): Effect.Effect => - Effect.gen(function* () { - const tempPath = `${filePath}.tmp`; - - try { - // Step 1: Write to temporary file - yield* Effect.promise(() => - PromiseFS.writeFile(tempPath, content, "utf-8") - ); - - yield* Effect.log(`[WRITE] Wrote to temporary file`); - - // Step 2: Ensure on disk (fsync) - yield* Effect.promise(() => - PromiseFS.writeFile(tempPath, content, "utf-8") - ); - - yield* Effect.log(`[FSYNC] Data on disk`); - - // Step 3: Atomic rename - yield* Effect.promise(() => - PromiseFS.rename(tempPath, filePath) - ); - - yield* Effect.log(`[RENAME] Atomic rename complete`); - } catch (error) { - // Cleanup on failure - try { - yield* Effect.promise(() => PromiseFS.unlink(tempPath)); - } catch { - // Ignore cleanup errors - } - - yield* Effect.fail(error); - } - }); - - // Test atomic write - const testFile = "./test-file.txt"; - - yield* atomicWrite(testFile, "Important configuration\n"); - - // Verify file - const content = yield* Effect.promise(() => - PromiseFS.readFile(testFile, "utf-8") - ); - - yield* Effect.log(`[READ] Got: "${content.trim()}"\n`); - - // Example 2: Streaming read (memory efficient) - console.log(`[2] Streaming read (handle large files):\n`); - - const streamingRead = (filePath: string) => - Effect.gen(function* () { - let byteCount = 0; - let lineCount = 0; - - const readStream = FS.createReadStream(filePath, { - encoding: "utf-8", - highWaterMark: 64 * 1024, // 64KB chunks - }); - - yield* Effect.log(`[STREAM] Starting read with 64KB chunks`); - - const processLine = (line: string) => - Effect.gen(function* () { - byteCount += line.length; - lineCount++; - - if (lineCount <= 2 || lineCount % 1000 === 0) { - yield* Effect.log( - `[LINE ${lineCount}] Length: ${line.length} bytes` - ); - } - }); - - // In real code, process all lines - yield* processLine("line 1"); - yield* processLine("line 2"); - - yield* Effect.log( - `[TOTAL] Read ${lineCount} lines, ${byteCount} bytes` - ); - }); - - yield* streamingRead(testFile); - - // Example 3: Recursive directory listing - console.log(`\n[3] Recursive directory traversal:\n`); - - const recursiveList = ( - dir: string, - maxDepth: number = 3 - ): Effect.Effect> => - Effect.gen(function* () { - const results: Array<{ path: string; type: "file" | "dir" }> = []; - - const traverse = (currentDir: string, depth: number) => - Effect.gen(function* () { - if (depth > maxDepth) { - return; - } - - const entries = yield* Effect.promise(() => - PromiseFS.readdir(currentDir, { withFileTypes: true }) - ); - - for (const entry of entries) { - const fullPath = Path.join(currentDir, entry.name); - - if (entry.isDirectory()) { - results.push({ path: fullPath, type: "dir" }); - - yield* traverse(fullPath, depth + 1); - } else { - results.push({ path: fullPath, type: "file" }); - } - } - }); - - yield* traverse(dir, 0); - - return results; - }); - - // List files in current directory - const entries = yield* recursiveList(".", 1); - - yield* Effect.log( - `[ENTRIES] Found ${entries.length} items:` - ); - - for (const entry of entries.slice(0, 5)) { - const type = entry.type === "file" ? "📄" : "📁"; - - yield* Effect.log(` ${type} ${entry.path}`); - } - - // Example 4: Bulk file operations - console.log(`\n[4] Bulk operations (efficient batching):\n`); - - const bulkCreate = (files: Array<{ name: string; content: string }>) => - Effect.gen(function* () { - yield* Effect.log(`[BULK] Creating ${files.length} files...`); - - for (const file of files) { - yield* atomicWrite(`./${file.name}`, file.content); - } - - yield* Effect.log(`[BULK] Created ${files.length} files`); - }); - - const testFiles = [ - { name: "config1.txt", content: "Config 1" }, - { name: "config2.txt", content: "Config 2" }, - { name: "config3.txt", content: "Config 3" }, - ]; - - yield* bulkCreate(testFiles); - - // Example 5: File watching (detect changes) - console.log(`\n[5] File watching (react to changes):\n`); - - const watchFile = (filePath: string) => - Effect.gen(function* () { - yield* Effect.log(`[WATCH] Starting to watch: ${filePath}`); - - let changeCount = 0; - - // Simulate file watcher - const checkForChanges = () => - Effect.gen(function* () { - for (let i = 0; i < 3; i++) { - yield* Effect.sleep("100 millis"); - - // Check file modification time - const stat = yield* Effect.promise(() => - PromiseFS.stat(filePath) - ); - - // In real implementation, compare previous mtime - if (i === 1) { - changeCount++; - - yield* Effect.log( - `[CHANGE] File modified (${stat.size} bytes)` - ); - } - } - }); - - yield* checkForChanges(); - - yield* Effect.log(`[WATCH] Detected ${changeCount} changes`); - }); - - yield* watchFile(testFile); - - // Example 6: Safe concurrent file operations - console.log(`\n[6] Concurrent file operations with safety:\n`); - - const lockFile = (filePath: string) => - Effect.gen(function* () { - const lockPath = `${filePath}.lock`; - - // Acquire lock - yield* atomicWrite(lockPath, "locked"); - - yield* Effect.log(`[LOCK] Acquired: ${lockPath}`); - - try { - // Critical section - yield* Effect.sleep("50 millis"); - - yield* Effect.log(`[CRITICAL] Operating on locked file`); - } finally { - // Release lock - yield* Effect.promise(() => - PromiseFS.unlink(lockPath) - ); - - yield* Effect.log(`[UNLOCK] Released: ${lockPath}`); - } - }); - - yield* lockFile(testFile); - - // Example 7: Efficient file copying - console.log(`\n[7] Efficient file copying:\n`); - - const efficientCopy = ( - source: string, - destination: string - ): Effect.Effect => - Effect.gen(function* () { - const stat = yield* Effect.promise(() => - PromiseFS.stat(source) - ); - - yield* Effect.log( - `[COPY] Reading ${(stat.size / 1024).toFixed(2)}KB` - ); - - const content = yield* Effect.promise(() => - PromiseFS.readFile(source) - ); - - yield* atomicWrite(destination, content.toString()); - - yield* Effect.log(`[COPY] Complete: ${destination}`); - }); - - yield* efficientCopy(testFile, "./test-file-copy.txt"); - - // Cleanup - yield* Effect.log(`\n[CLEANUP] Removing test files`); - - for (const name of [testFile, "test-file-copy.txt", ...testFiles.map((f) => `./${f.name}`)]) { - try { - yield* Effect.promise(() => - PromiseFS.unlink(name) - ); - - yield* Effect.log(`[REMOVED] ${name}`); - } catch { - // File doesn't exist, that's ok - } - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Simple file operations cause problems at scale: - -**Problem 1: Corrupted files** -- Write config file -- Server crashes mid-write -- File is partial/corrupted -- Application fails to start -- Production outage - -**Problem 2: Large file handling** -- Load 10GB file into memory -- Server runs out of memory -- Everything crashes -- Now handling outages instead of serving - -**Problem 3: Directory synchronization** -- Copy directory tree -- Process interrupted -- Some files copied, some not -- Directory in inconsistent state -- Hard to recover - -**Problem 4: Inefficient updates** -- Update 10,000 files one by one -- Each file system call is slow -- Takes hours -- Meanwhile, users can't access data - -**Problem 5: File locking** -- Process A reads file -- Process B writes file -- Process A gets partially written file -- Data corruption - -Solutions: - -**Atomic writes**: -- Write to temporary file -- Fsync (guarantee on disk) -- Atomic rename -- No corruption even on crash - -**Streaming**: -- Process large files in chunks -- Keep memory constant -- Efficient for any file size - -**Bulk operations**: -- Batch multiple operations -- Reduce system calls -- Faster overall completion - -**File watching**: -- React to changes -- Avoid polling -- Real-time responsiveness - ---- - ---- - -## Poll for Status Until a Task Completes - -**Rule:** Use Effect.race to run a repeating polling task that is automatically interrupted when a main task completes. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This program simulates a long-running data processing job. While it's running, a separate effect polls for its status every 2 seconds. When the main job finishes after 10 seconds, the polling automatically stops. - -```typescript -import { Effect, Schedule, Duration } from "effect"; - -// The main task that takes a long time to complete -const longRunningJob = Effect.log("Data processing complete!").pipe( - Effect.delay(Duration.seconds(10)) -); - -// The polling task that checks the status -const pollStatus = Effect.log("Polling for job status: In Progress..."); - -// A schedule that repeats the polling task every 2 seconds, forever -const pollingSchedule = Schedule.fixed(Duration.seconds(2)); - -// The complete polling effect that will run indefinitely until interrupted -const repeatingPoller = pollStatus.pipe(Effect.repeat(pollingSchedule)); - -// Race the main job against the poller. -// The longRunningJob will win after 10 seconds, interrupting the poller. -const program = Effect.race(longRunningJob, repeatingPoller); - -Effect.runPromise(program); -/* -Output: -Polling for job status: In Progress... -Polling for job status: In Progress... -Polling for job status: In Progress... -Polling for job status: In Progress... -Polling for job status: In Progress... -Data processing complete! -*/ -``` - ---- - -### Anti-Pattern - -Manually managing the lifecycle of the polling fiber. This is more verbose, imperative, and error-prone. You have to remember to interrupt the polling fiber in all possible exit paths (success, failure, etc.), which `Effect.race` does for you automatically. - -```typescript -import { Effect, Fiber } from "effect"; -import { longRunningJob, repeatingPoller } from "./somewhere"; - -// ❌ WRONG: Manual fiber management is complex. -const program = Effect.gen(function* () { - // Manually fork the poller into the background - const pollerFiber = yield* Effect.fork(repeatingPoller); - - try { - // Run the main job - const result = yield* longRunningJob; - return result; - } finally { - // You MUST remember to interrupt the poller when you're done. - yield* Fiber.interrupt(pollerFiber); - } -}); -``` - -### Explanation - -This pattern elegantly solves the problem of coordinating a long-running job with a status-checking mechanism. Instead of manually managing fibers with `fork` and `interrupt`, you can declare this relationship with `Effect.race`. - -The key is that the polling effect is set up to repeat on a schedule that runs indefinitely (or for a very long time). Because it never completes on its own, it can never "win" the race. The main task is the only one that can complete successfully. When it does, it wins the race, and Effect's structured concurrency guarantees that the losing effect (the poller) is safely interrupted. - -This creates a self-contained, declarative, and leak-free unit of work. - ---- - ---- - -## Pool Resources for Reuse - -**Rule:** Use Pool to manage expensive resources that can be reused across operations. - -**Skill Level:** intermediate - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Pool, Scope, Duration } from "effect" - -// ============================================ -// 1. Define a poolable resource -// ============================================ - -interface DatabaseConnection { - readonly id: number - readonly query: (sql: string) => Effect.Effect - readonly close: () => Effect.Effect -} - -let connectionId = 0 - -const createConnection = Effect.gen(function* () { - const id = ++connectionId - yield* Effect.log(`Creating connection ${id}`) - - // Simulate connection setup time - yield* Effect.sleep("100 millis") - - const connection: DatabaseConnection = { - id, - query: (sql) => Effect.gen(function* () { - yield* Effect.log(`[Conn ${id}] Executing: ${sql}`) - return [{ result: "data" }] - }), - close: () => Effect.gen(function* () { - yield* Effect.log(`Closing connection ${id}`) - }), - } - - return connection -}) - -// ============================================ -// 2. Create a pool -// ============================================ - -const makeConnectionPool = Pool.make({ - acquire: createConnection, - size: 5, // Maximum 5 connections -}) - -// ============================================ -// 3. Use the pool -// ============================================ - -const runQuery = (pool: Pool.Pool, sql: string) => - Effect.scoped( - Effect.gen(function* () { - // Get a connection from the pool - const connection = yield* pool.get - - // Use it - const results = yield* connection.query(sql) - - // Connection automatically returned to pool when scope ends - return results - }) - ) - -// ============================================ -// 4. Run multiple queries concurrently -// ============================================ - -const program = Effect.scoped( - Effect.gen(function* () { - const pool = yield* makeConnectionPool - - yield* Effect.log("Starting concurrent queries...") - - // Run 10 queries with only 5 connections - const queries = Array.from({ length: 10 }, (_, i) => - runQuery(pool, `SELECT * FROM users WHERE id = ${i}`) - ) - - const results = yield* Effect.all(queries, { concurrency: "unbounded" }) - - yield* Effect.log(`Completed ${results.length} queries`) - return results - }) -) - -Effect.runPromise(program) -``` - -### Explanation - -Creating resources is expensive: - -1. **Database connections** - TCP handshake, authentication -2. **HTTP clients** - Connection setup, TLS negotiation -3. **Worker threads** - Spawn overhead -4. **File handles** - System calls - -Pooling amortizes this cost across many operations. - ---- - ---- - -## 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. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good 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 { Clock, 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 = yield* Clock.currentTimeMillis; - const users = yield* Effect.forEach(userIds, fetchUserById, { - concurrency: 5, // Limit to 5 concurrent operations - }); - const endTime = yield* Clock.currentTimeMillis; - - 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); -``` - ---- - -### Anti-Pattern - -The anti-pattern is using `Effect.all` to process a large or dynamically-sized collection. This can lead to unpredictable and potentially catastrophic resource consumption. - -```typescript -import { Effect } from "effect"; -import { userIds, fetchUserById } from "./somewhere"; // From previous example - -// ❌ DANGEROUS: This will attempt to start 100 concurrent network requests. -// If userIds had 10,000 items, this could crash your application or get you blocked by an API. -const program = Effect.all(userIds.map(fetchUserById)); -``` - -### Explanation - -Running `Effect.all` on a large array of tasks is dangerous. If you have 1,000 items, it will try to start 1,000 concurrent fibers at once, which can exhaust memory, overwhelm your CPU, or hit API rate limits. - -`Effect.forEach` with a concurrency limit solves this problem elegantly. It acts as a concurrent processing pool. It will start processing items up to your specified limit (e.g., 10 at a time). As soon as one task finishes, it will pick up the next available item from the list, ensuring that no more than 10 tasks are ever running simultaneously. This provides massive performance gains over sequential processing while maintaining stability and control. - ---- - ---- - -## Process a Large File with Constant Memory - -**Rule:** Use Stream.fromReadable with a Node.js Readable stream to process files efficiently. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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 STREAMING pipeline - reads file in chunks, not all at once - const fileStream = fs.readFile(filePath).pipe( - // Decode bytes to text - Stream.decodeText("utf-8"), - // Split into lines - Stream.splitLines, - // 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))); - -/* -Output: -... level=INFO msg="Processing: line 1" -... level=INFO msg="Processing: line 2" -... level=INFO msg="Processing: line 3" -*/ -``` - -### Anti-Pattern - -The anti-pattern is to use synchronous, memory-intensive functions like `fs.readFileSync`. This approach is simple for tiny files but fails catastrophically for large ones. - -```typescript -import * as fs from "node:fs"; -import * as path from "node:path"; - -const filePath = path.join(__dirname, "large-file.txt"); -// Create a dummy file for the example -fs.writeFileSync(filePath, "line 1\nline 2\nline 3"); - -try { - // Anti-pattern: This loads the ENTIRE file into memory as a single buffer. - const fileContent = fs.readFileSync(filePath, "utf-8"); - const lines = fileContent.split("\n"); - - for (const line of lines) { - console.log(`Processing: ${line}`); - } -} catch (err) { - console.error("Failed to read file:", err); -} finally { - // Clean up the dummy file - fs.unlinkSync(filePath); -} -``` - -This is a dangerous anti-pattern because: - -1. **It's a Memory Bomb**: If `large-file.txt` were 2GB and your server had 1GB of RAM, this code would immediately crash the process. -2. **It Blocks the Event Loop**: `readFileSync` is a synchronous, blocking operation. While it's reading the file from disk, your entire application is frozen and cannot respond to any other requests. -3. **It's Not Composable**: You get a giant string that must be processed eagerly. You lose all the benefits of lazy processing, concurrency control, and integrated error handling that `Stream` provides. - -### Explanation - -The most significant advantage of a streaming architecture is its ability to handle datasets far larger than available RAM. When you need to process a multi-gigabyte log file or CSV, loading it all into memory is not an option—it will crash your application. - -The `Stream.fromReadable` constructor provides a bridge from Node.js's built-in file streaming capabilities to the Effect ecosystem. This approach is superior because: - -1. **Constant Memory Usage**: The file is read in small, manageable chunks. Your application's memory usage remains low and constant, regardless of whether the file is 1 megabyte or 100 gigabytes. -2. **Composability**: Once the file is represented as an Effect `Stream`, you can apply the full suite of powerful operators to it: `mapEffect` for concurrent processing, `filter` for selectively choosing lines, `grouped` for batching, and `retry` for resilience. -3. **Resource Safety**: Effect's `Stream` is built on `Scope`, which guarantees that the underlying file handle will be closed automatically when the stream finishes, fails, or is interrupted. This prevents resource leaks, a common problem in manual file handling. - ---- - ---- - -## Process collections of data asynchronously - -**Rule:** Leverage Stream to process collections effectfully with built-in concurrency control and resource safety. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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 -); - -const programWithLogging = Effect.gen(function* () { - const users = yield* program; - yield* Effect.log( - `All users fetched: ${JSON.stringify(Chunk.toArray(users))}` - ); - return users; -}); - -Effect.runPromise(programWithLogging); -``` - -### Anti-Pattern - -A common but flawed approach is to use `Promise.all` to handle multiple asynchronous operations. This method lacks the safety, control, and composability inherent to Effect's `Stream`. - -```typescript -// A mock function that returns a Promise -const getUserByIdAsPromise = ( - id: number -): Promise<{ id: number; name: string }> => - new Promise((resolve) => { - setTimeout(() => { - console.log(`Fetched user ${id}`); - resolve({ id, name: `User ${id}` }); - }, 100); - }); - -// The Promise-based program -const ids = [1, 2, 3, 4, 5]; -const promises = ids.map(getUserByIdAsPromise); - -Promise.all(promises).then((users) => { - console.log("All users fetched:", users); -}); -``` - -This anti-pattern is problematic because it immediately executes all promises in parallel with no concurrency limit, it does not benefit from Effect's structured concurrency for safe interruption, and it breaks out of the Effect context, losing composability with features like logging, retries, and dependency management. - -### Explanation - -`Stream` is a fundamental data type in Effect for handling collections of data, especially in asynchronous contexts. Unlike a simple array, a `Stream` is lazy and pull-based, meaning it only computes or fetches elements as they are needed, making it highly efficient for large or infinite datasets. - -The primary benefits of using `Stream` are: - -1. **Concurrency Control**: `Stream` provides powerful and simple operators like `mapEffect` that have built-in concurrency management. This prevents overwhelming downstream services with too many parallel requests. -2. **Resource Safety**: `Stream` is built on `Scope`, ensuring that any resources opened during the stream's operation (like file handles or network connections) are safely and reliably closed, even in the case of errors or interruption. -3. **Composability**: Streams are highly composable. They can be filtered, mapped, transformed, and combined with other Effect data types seamlessly, allowing you to build complex data processing pipelines that remain readable and type-safe. -4. **Resilience**: `Stream` integrates with `Schedule` to provide sophisticated retry and repeat logic, and with Effect's structured concurrency to ensure that failures in one part of a pipeline lead to a clean and predictable shutdown of the entire process. - ---- - ---- - -## Process Items Concurrently - -**Rule:** Use Stream.mapEffect with the `concurrency` option to process stream items in parallel. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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); - -const programWithLogging = Effect.gen(function* () { - const [duration, _] = yield* timedProgram; - const durationMs = Number(duration); - yield* Effect.log(`\nTotal time: ${Math.round(durationMs / 1000)} seconds`); - return duration; -}).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Program error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithLogging); -/* -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 -*/ -``` - -### Anti-Pattern - -The anti-pattern is to process I/O-bound tasks sequentially. This is the default behavior of `Stream.mapEffect` if you don't specify a concurrency level, and it leads to poor performance. - -```typescript -import { Effect, Stream } from "effect"; -// ... same processItem function ... - -const ids = [1, 2, 3, 4]; - -// Processing sequentially (default concurrency is 1) -const program = Stream.fromIterable(ids).pipe( - Stream.mapEffect(processItem), // No concurrency option - Stream.runDrain -); - -const timedProgram = Effect.timed(program); - -Effect.runPromise(timedProgram).then(([duration, _]) => { - console.log(`\nTotal time: ${Math.round(duration.millis / 1000)} seconds`); -}); -/* -Output: -... level=INFO msg="Starting item 1..." -... level=INFO msg="Finished item 1" -... level=INFO msg="Starting item 2..." -... level=INFO msg="Finished item 2" -... etc. - -Total time: 4 seconds -*/ -``` - -While sequential processing is sometimes necessary to preserve order or avoid race conditions, it is a performance anti-pattern for independent, I/O-bound tasks. The concurrent approach is almost always preferable in such cases. - -### Explanation - -For many data pipelines, the most time-consuming step is performing an I/O-bound operation for each item, such as calling an API or querying a database. Processing these items one by one (sequentially) is safe but slow, as the entire pipeline waits for each operation to complete before starting the next. - -`Stream.mapEffect`'s `concurrency` option is the solution. It provides a simple, declarative way to introduce controlled parallelism into your pipeline. - -1. **Performance Boost**: It allows the stream to work on multiple items at once, drastically reducing the total execution time for I/O-bound tasks. -2. **Controlled Parallelism**: Unlike `Promise.all` which runs everything at once, you specify the _exact_ number of concurrent operations. This is crucial for stability, as it prevents your application from overwhelming downstream services or exhausting its own resources (like file handles or network sockets). -3. **Automatic Backpressure**: The stream will not pull new items from the source faster than the concurrent slots can process them. This backpressure is handled automatically, preventing memory issues. -4. **Structured Concurrency**: It's fully integrated with Effect's runtime. If any concurrent operation fails, all other in-flight operations for that stream are immediately and reliably interrupted, preventing wasted work and ensuring clean shutdowns. - ---- - ---- - -## Process Items in Batches - -**Rule:** Use Stream.grouped(n) to transform a stream of items into a stream of batched chunks. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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" -*/ -``` - -### Anti-Pattern - -The anti-pattern is to process items one by one when a more efficient bulk operation is available. This is a common performance bottleneck. - -```typescript -import { Effect, Stream } from "effect"; - -// A mock function that saves one user at a time -const saveUser = (user: { id: number }): Effect.Effect => - Effect.log(`Saving single user: ${user.id}`); - -const userIds = Array.from({ length: 10 }, (_, i) => ({ id: i + 1 })); - -const program = Stream.fromIterable(userIds).pipe( - // Process each user individually, leading to 10 separate "saves" - Stream.mapEffect(saveUser, { concurrency: 1 }), - Stream.runDrain -); - -Effect.runPromise(program); -/* -Output: -... level=INFO msg="Saving single user: 1" -... level=INFO msg="Saving single user: 2" -... (and so on for all 10 users) -*/ -``` - -This individual processing approach is an anti-pattern because it creates unnecessary overhead. If each `saveUser` call took 50ms of network latency, the total time would be over 500ms. The batched approach might only take 100ms (2 batches \* 50ms), resulting in a 5x performance improvement. - -### Explanation - -When interacting with external systems like databases or APIs, making one request per item is often incredibly inefficient. The network latency and overhead of each individual call can dominate the total processing time. Most high-performance systems offer bulk or batch endpoints to mitigate this. - -`Stream.grouped(n)` provides a simple, declarative way to prepare your data for these bulk operations: - -1. **Performance Optimization**: It dramatically reduces the number of network roundtrips. A single API call with 100 items is far faster than 100 individual API calls. -2. **Declarative Batching**: It abstracts away the tedious and error-prone manual logic of counting items, managing temporary buffers, and deciding when to send a batch. -3. **Seamless Composition**: It transforms a `Stream` into a `Stream>`. This new stream of chunks can be piped directly into `Stream.mapEffect`, allowing you to process each batch concurrently. -4. **Handles Leftovers**: The operator automatically handles the final, smaller batch if the total number of items is not perfectly divisible by the batch size. - ---- - ---- - -## Process Streaming Data with Stream - -**Rule:** Use Stream to model and process data that arrives over time in a composable, efficient way. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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}`) -); - -const programWithErrorHandling = program.pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Stream processing error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithErrorHandling); -``` - ---- - -### Anti-Pattern - -Manually managing pagination state with recursive functions. This is complex, stateful, and easy to get wrong. It also requires loading all results into memory, which is inefficient for large datasets. - -```typescript -import { Effect } from "effect"; -import { fetchUserPage } from "./somewhere"; // From previous example - -// ❌ WRONG: Manual, stateful, and inefficient recursion. -const fetchAllUsers = ( - page: number, - acc: any[] -): Effect.Effect => - fetchUserPage(page).pipe( - Effect.flatMap((response) => { - const allUsers = [...acc, ...response.users]; - if (response.nextPage) { - return fetchAllUsers(response.nextPage, allUsers); - } - return Effect.succeed(allUsers); - }) - ); - -// This holds all users in memory at once. -const program = fetchAllUsers(0, []); -``` - -### Explanation - -Some data sources don't fit the one-shot request/response model of `Effect`. For example: - -- Reading a multi-gigabyte file from disk. -- Receiving messages from a WebSocket. -- Fetching results from a paginated API. - -Loading all this data into memory at once would be inefficient or impossible. `Stream` solves this by allowing you to process the data in chunks as it arrives. It provides a rich API of composable operators (`map`, `filter`, `run`, etc.) that mirror those on `Effect` and `Array`, but are designed for streaming data. This allows you to build efficient, constant-memory data processing pipelines. - ---- - ---- - -## Profile Effect Applications - -**Rule:** Use Effect's timing features and Node.js profilers to find performance bottlenecks. - -**Skill Level:** advanced - -**Use Cases:** tooling-and-debugging - -### Good Example - -### 1. Basic Timing with Spans - -```typescript -import { Effect, Duration } from "effect" - -// ============================================ -// 1. Time individual operations -// ============================================ - -const timeOperation = ( - name: string, - effect: Effect.Effect -) => - Effect.gen(function* () { - const startTime = Date.now() - - const result = yield* effect - - const duration = Date.now() - startTime - yield* Effect.log(`${name}: ${duration}ms`) - - return result - }) - -// Usage -const program = Effect.gen(function* () { - yield* timeOperation("database-query", queryDatabase()) - yield* timeOperation("api-call", callExternalApi()) - yield* timeOperation("processing", processData()) -}) - -// ============================================ -// 2. Use withLogSpan for nested timing -// ============================================ - -const timedProgram = Effect.gen(function* () { - yield* Effect.log("Starting") - - yield* fetchUsers().pipe(Effect.withLogSpan("fetchUsers")) - - yield* processUsers().pipe(Effect.withLogSpan("processUsers")) - - yield* saveResults().pipe(Effect.withLogSpan("saveResults")) - - yield* Effect.log("Complete") -}).pipe(Effect.withLogSpan("total")) - -// ============================================ -// 3. Collect timing metrics -// ============================================ - -import { Metric } from "effect" - -const operationDuration = Metric.histogram("operation_duration_ms", { - description: "Operation duration in milliseconds", - boundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000], -}) - -const profiledEffect = ( - name: string, - effect: Effect.Effect -) => - Effect.gen(function* () { - const startTime = Date.now() - - const result = yield* effect - - const duration = Date.now() - startTime - yield* Metric.update( - operationDuration.pipe(Metric.tagged("operation", name)), - duration - ) - - return result - }) - -// ============================================ -// 4. Memory profiling -// ============================================ - -const logMemoryUsage = Effect.sync(() => { - const usage = process.memoryUsage() - return { - heapUsed: Math.round(usage.heapUsed / 1024 / 1024), - heapTotal: Math.round(usage.heapTotal / 1024 / 1024), - external: Math.round(usage.external / 1024 / 1024), - rss: Math.round(usage.rss / 1024 / 1024), - } -}) - -const withMemoryLogging = (effect: Effect.Effect) => - Effect.gen(function* () { - const before = yield* logMemoryUsage - yield* Effect.log(`Memory before: ${JSON.stringify(before)}MB`) - - const result = yield* effect - - const after = yield* logMemoryUsage - yield* Effect.log(`Memory after: ${JSON.stringify(after)}MB`) - yield* Effect.log(`Memory delta: ${after.heapUsed - before.heapUsed}MB`) - - return result - }) - -// ============================================ -// 5. CPU profiling with Node.js inspector -// ============================================ - -const withCpuProfile = ( - name: string, - effect: Effect.Effect -) => - Effect.gen(function* () { - // Start CPU profiler (requires --inspect flag) - const inspector = yield* Effect.try(() => { - const { Session } = require("inspector") - const session = new Session() - session.connect() - return session - }) - - yield* Effect.try(() => { - inspector.post("Profiler.enable") - inspector.post("Profiler.start") - }) - - const result = yield* effect - - // Stop and save profile - yield* Effect.async((resume) => { - inspector.post("Profiler.stop", (err: Error, { profile }: any) => { - if (err) { - resume(Effect.fail(err)) - } else { - const fs = require("fs") - fs.writeFileSync( - `${name}-${Date.now()}.cpuprofile`, - JSON.stringify(profile) - ) - resume(Effect.void) - } - }) - }) - - return result - }) - -// ============================================ -// 6. Benchmark specific operations -// ============================================ - -const benchmark = ( - name: string, - effect: Effect.Effect, - iterations: number = 100 -) => - Effect.gen(function* () { - const times: number[] = [] - - for (let i = 0; i < iterations; i++) { - const start = performance.now() - yield* effect - times.push(performance.now() - start) - } - - const sorted = times.sort((a, b) => a - b) - const stats = { - min: sorted[0], - max: sorted[sorted.length - 1], - median: sorted[Math.floor(sorted.length / 2)], - p95: sorted[Math.floor(sorted.length * 0.95)], - p99: sorted[Math.floor(sorted.length * 0.99)], - mean: times.reduce((a, b) => a + b, 0) / times.length, - } - - yield* Effect.log(`Benchmark "${name}" (${iterations} iterations):`) - yield* Effect.log(` Min: ${stats.min.toFixed(2)}ms`) - yield* Effect.log(` Max: ${stats.max.toFixed(2)}ms`) - yield* Effect.log(` Mean: ${stats.mean.toFixed(2)}ms`) - yield* Effect.log(` Median: ${stats.median.toFixed(2)}ms`) - yield* Effect.log(` P95: ${stats.p95.toFixed(2)}ms`) - yield* Effect.log(` P99: ${stats.p99.toFixed(2)}ms`) - - return stats - }) - -// ============================================ -// 7. Profile concurrent operations -// ============================================ - -const profileConcurrency = Effect.gen(function* () { - const items = Array.from({ length: 100 }, (_, i) => i) - - // Sequential - yield* benchmark( - "sequential", - Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 1 }), - 10 - ) - - // Parallel unbounded - yield* benchmark( - "parallel-unbounded", - Effect.forEach(items, (i) => Effect.succeed(i * 2), { - concurrency: "unbounded", - }), - 10 - ) - - // Parallel limited - yield* benchmark( - "parallel-10", - Effect.forEach(items, (i) => Effect.succeed(i * 2), { concurrency: 10 }), - 10 - ) -}) - -// ============================================ -// 8. Run profiling -// ============================================ - -const profilingSession = Effect.gen(function* () { - yield* Effect.log("=== Profiling Session ===") - - yield* withMemoryLogging( - benchmark("my-operation", someEffect, 50) - ) - - yield* profileConcurrency -}) - -Effect.runPromise(profilingSession) -``` - -### Explanation - -Profiling helps you: - -1. **Find bottlenecks** - What's slow? -2. **Optimize hot paths** - Focus effort where it matters -3. **Track regressions** - Catch slowdowns early -4. **Right-size resources** - Don't over-provision - ---- - ---- - -## Property-Based Testing with Effect - -**Rule:** Use property-based testing to find edge cases your example-based tests miss. - -**Skill Level:** advanced - -**Use Cases:** testing - -### Good Example - -```typescript -import { describe, it, expect } from "vitest" -import { Effect, Option, Either, Schema } from "effect" -import * as fc from "fast-check" - -describe("Property-Based Testing with Effect", () => { - // ============================================ - // 1. Test pure function properties - // ============================================ - - it("should satisfy array reverse properties", () => { - fc.assert( - fc.property(fc.array(fc.integer()), (arr) => { - // Reversing twice returns original - const reversed = arr.slice().reverse() - const doubleReversed = reversed.slice().reverse() - - return JSON.stringify(arr) === JSON.stringify(doubleReversed) - }) - ) - }) - - it("should satisfy sort idempotence", () => { - fc.assert( - fc.property(fc.array(fc.integer()), (arr) => { - const sorted = arr.slice().sort((a, b) => a - b) - const sortedTwice = sorted.slice().sort((a, b) => a - b) - - return JSON.stringify(sorted) === JSON.stringify(sortedTwice) - }) - ) - }) - - // ============================================ - // 2. Test Effect operations - // ============================================ - - it("should map then flatMap equals flatMap with mapping", async () => { - await fc.assert( - fc.asyncProperty(fc.integer(), async (n) => { - const f = (x: number) => x * 2 - const g = (x: number) => Effect.succeed(x + 1) - - // map then flatMap - const result1 = await Effect.runPromise( - Effect.succeed(n).pipe( - Effect.map(f), - Effect.flatMap(g) - ) - ) - - // flatMap with mapping inside - const result2 = await Effect.runPromise( - Effect.succeed(n).pipe( - Effect.flatMap((x) => g(f(x))) - ) - ) - - return result1 === result2 - }) - ) - }) - - // ============================================ - // 3. Test Option properties - // ============================================ - - it("should satisfy Option map identity", () => { - fc.assert( - fc.property(fc.option(fc.integer(), { nil: undefined }), (maybeN) => { - const option = maybeN === undefined ? Option.none() : Option.some(maybeN) - - // Mapping identity function returns same Option - const mapped = Option.map(option, (x) => x) - - return Option.getOrElse(option, () => -1) === - Option.getOrElse(mapped, () => -1) - }) - ) - }) - - // ============================================ - // 4. Test Schema encode/decode roundtrip - // ============================================ - - it("should roundtrip through Schema", async () => { - const UserSchema = Schema.Struct({ - name: Schema.String, - age: Schema.Number.pipe(Schema.int(), Schema.positive()), - }) - - const userArbitrary = fc.record({ - name: fc.string({ minLength: 1 }), - age: fc.integer({ min: 1, max: 120 }), - }) - - await fc.assert( - fc.asyncProperty(userArbitrary, async (user) => { - const encode = Schema.encode(UserSchema) - const decode = Schema.decode(UserSchema) - - // Encode then decode should return equivalent value - const encoded = await Effect.runPromise(encode(user)) - const decoded = await Effect.runPromise(decode(encoded)) - - return decoded.name === user.name && decoded.age === user.age - }) - ) - }) - - // ============================================ - // 5. Test error handling properties - // ============================================ - - it("should recover from any error", async () => { - await fc.assert( - fc.asyncProperty( - fc.string(), - fc.string(), - async (errorMsg, fallback) => { - const failing = Effect.fail(new Error(errorMsg)) - - const result = await Effect.runPromise( - failing.pipe( - Effect.catchAll(() => Effect.succeed(fallback)) - ) - ) - - return result === fallback - } - ) - ) - }) - - // ============================================ - // 6. Custom generators for domain types - // ============================================ - - interface Email { - readonly _tag: "Email" - readonly value: string - } - - const emailArbitrary = fc.emailAddress().map((value): Email => ({ - _tag: "Email", - value, - })) - - interface UserId { - readonly _tag: "UserId" - readonly value: string - } - - const userIdArbitrary = fc.uuid().map((value): UserId => ({ - _tag: "UserId", - value, - })) - - it("should handle domain types correctly", () => { - fc.assert( - fc.property(emailArbitrary, userIdArbitrary, (email, userId) => { - // Test your domain functions with generated domain types - return email.value.includes("@") && userId.value.length > 0 - }) - ) - }) - - // ============================================ - // 7. Test algebraic properties - // ============================================ - - it("should satisfy monoid properties for string concat", () => { - const empty = "" - const concat = (a: string, b: string) => a + b - - fc.assert( - fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => { - // Identity: empty + a = a = a + empty - const leftIdentity = concat(empty, a) === a - const rightIdentity = concat(a, empty) === a - - // Associativity: (a + b) + c = a + (b + c) - const associative = concat(concat(a, b), c) === concat(a, concat(b, c)) - - return leftIdentity && rightIdentity && associative - }) - ) - }) - - // ============================================ - // 8. Test with constraints - // ============================================ - - it("should handle positive numbers", () => { - fc.assert( - fc.property( - fc.integer({ min: 1, max: 1000000 }), - fc.integer({ min: 1, max: 1000000 }), - (a, b) => { - // Division of positives is positive - const result = a / b - return result > 0 - } - ) - ) - }) -}) -``` - -### Explanation - -Property-based testing finds bugs that example tests miss: - -1. **Edge cases** - Empty arrays, negative numbers, unicode -2. **Invariants** - Properties that should always hold -3. **Shrinking** - Minimal failing examples -4. **Coverage** - Many inputs from one test - ---- - ---- - -## Provide Configuration to Your App via a Layer - -**Rule:** Provide configuration to your app via a Layer. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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}...`); -}); - -const programWithErrorHandling = Effect.provide( - program, - ServerConfig.Default -).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Program error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithErrorHandling); -``` - -**Explanation:** -This approach makes configuration available contextually, supporting better testing and modularity. - -### Anti-Pattern - -Manually reading environment variables deep inside business logic. This tightly couples that logic to the external environment, making it difficult to test and reuse. - -### Explanation - -Integrating configuration as a `Layer` plugs it directly into Effect's dependency injection system. This makes your configuration available anywhere in the program and dramatically simplifies testing by allowing you to substitute mock configuration. - ---- - -## Provide Dependencies to Routes - -**Rule:** Define dependencies with Effect.Service and provide them to your HTTP server using a Layer. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good 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); -``` - -### Anti-Pattern - -The anti-pattern is to manually instantiate and pass dependencies through function arguments. This creates tight coupling and makes testing difficult. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -// Manual implementation of a database client -class LiveDatabase { - getUser(id: string) { - if (id === "123") { - return Effect.succeed({ name: "Paul" }); - } - return Effect.fail("User not found"); // Untyped error - } -} - -// The dependency must be passed explicitly to the route definition -const createGetUserRoute = (db: LiveDatabase) => - Http.router.get( - "/users/:userId", - Effect.flatMap(Http.request.ServerRequest, (req) => - db.getUser(req.params.userId) - ).pipe( - Effect.map(Http.response.json), - Effect.catchAll(() => Http.response.empty({ status: 404 })) - ) - ); - -// Manually instantiate the dependency -const db = new LiveDatabase(); -const getUserRoute = createGetUserRoute(db); - -const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute)); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This approach is flawed because the route handler is now aware of the concrete `LiveDatabase` class. Swapping it for a mock in a test would be cumbersome. Furthermore, if a service deep within the call stack needs a dependency, it must be "drilled" down through every intermediate function, which is a significant maintenance burden. - -### Explanation - -As applications grow, route handlers need to perform complex tasks like accessing a database, calling other APIs, or logging. Hard-coding this logic or manually passing dependencies leads to tightly coupled, untestable code. - -Effect's dependency injection system (`Service` and `Layer`) solves this by decoupling a service's interface from its implementation. This is the cornerstone of building scalable, maintainable applications in Effect. - -1. **Modern and Simple**: `Effect.Service` is the modern, idiomatic way to define services. It combines the service's definition and its access tag into a single, clean class structure, reducing boilerplate. -2. **Testability**: By depending on a service interface, you can easily provide a mock implementation in your tests (e.g., `Database.Test`) instead of the real one (`Database.Live`), allowing for fast, isolated unit tests of your route logic. -3. **Decoupling**: Route handlers don't know or care _how_ the database connection is created or managed. They simply ask for the `Database` service from the context, and the runtime provides the configured implementation. -4. **Composability**: `Layer`s are composable. You can build complex dependency graphs (e.g., a `Database` layer that itself requires a `Config` layer) that Effect will automatically construct and wire up for you. - ---- - ---- - -## 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. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good 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. -const programWithResults = Effect.gen(function* () { - try { - const user = yield* program; - yield* Effect.log(`User found: ${JSON.stringify(user)}`); - return user; - } catch (error) { - yield* Effect.logError(`Error: ${error}`); - throw error; - } -}).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Handled error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithResults); - -// 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); -``` - ---- - -### Anti-Pattern - -Don't use `Effect.race` if you need the results of _all_ the effects. That is the job of `Effect.all`. Using `race` in this scenario will cause you to lose data, as all but one of the effects will be interrupted and their results discarded. - -```typescript -import { Effect } from "effect"; - -const fetchProfile = Effect.succeed({ name: "Paul" }); -const fetchPermissions = Effect.succeed(["admin", "editor"]); - -// ❌ WRONG: This will only return either the profile OR the permissions, -// whichever resolves first. You will lose the other piece of data. -const incompleteData = Effect.race(fetchProfile, fetchPermissions); - -// ✅ CORRECT: Use Effect.all when you need all the results. -const completeData = Effect.all([fetchProfile, fetchPermissions]); -``` - -### Explanation - -`Effect.race` is a powerful concurrency primitive for performance and resilience. It starts all provided effects in parallel. The moment one of them succeeds, `Effect.race` immediately interrupts all the other "losing" effects and returns the winning result. If one of the effects fails before any have succeeded, the race is not over; the remaining effects continue to run. The entire race only fails if _all_ participating effects fail. - -This is commonly used for: - -- **Performance:** Querying multiple redundant data sources (e.g., two API replicas) and taking the response from whichever is faster. -- **Implementing Timeouts:** Racing a primary effect against a delayed `Effect.fail`, effectively creating a timeout mechanism. - ---- - ---- - -## Race Effects and Handle Timeouts - -**Rule:** Use Effect.race for fastest-wins, Effect.timeout for time limits. - -**Skill Level:** beginner - -**Use Cases:** concurrency-getting-started - -### Good Example - -```typescript -import { Effect, Option } from "effect" - -// ============================================ -// BASIC RACE: First one wins -// ============================================ - -const server1 = Effect.gen(function* () { - yield* Effect.sleep("100 millis") - return "Response from server 1" -}) - -const server2 = Effect.gen(function* () { - yield* Effect.sleep("50 millis") - return "Response from server 2" -}) - -const raceServers = Effect.race(server1, server2) - -Effect.runPromise(raceServers).then((result) => { - console.log(result) // "Response from server 2" (faster) -}) - -// ============================================ -// BASIC TIMEOUT: Limit execution time -// ============================================ - -const slowOperation = Effect.gen(function* () { - yield* Effect.sleep("5 seconds") - return "Finally done" -}) - -// Returns Option.none if timeout -const withTimeout = slowOperation.pipe( - Effect.timeout("1 second") -) - -Effect.runPromise(withTimeout).then((result) => { - if (Option.isNone(result)) { - console.log("Operation timed out") - } else { - console.log(`Got: ${result.value}`) - } -}) - -// ============================================ -// TIMEOUT WITH FALLBACK -// ============================================ - -const withFallback = slowOperation.pipe( - Effect.timeoutTo({ - duration: "1 second", - onTimeout: () => Effect.succeed("Using cached value"), - }) -) - -Effect.runPromise(withFallback).then((result) => { - console.log(result) // "Using cached value" -}) - -// ============================================ -// TIMEOUT FAIL: Throw error on timeout -// ============================================ - -class TimeoutError { - readonly _tag = "TimeoutError" -} - -const failOnTimeout = slowOperation.pipe( - Effect.timeoutFail({ - duration: "1 second", - onTimeout: () => new TimeoutError(), - }) -) - -// ============================================ -// RACE ALL: Multiple competing effects -// ============================================ - -const fetchFromCache = Effect.gen(function* () { - yield* Effect.sleep("10 millis") - return { source: "cache", data: "cached data" } -}) - -const fetchFromDB = Effect.gen(function* () { - yield* Effect.sleep("100 millis") - return { source: "db", data: "fresh data" } -}) - -const fetchFromAPI = Effect.gen(function* () { - yield* Effect.sleep("200 millis") - return { source: "api", data: "api data" } -}) - -const raceAll = Effect.raceAll([fetchFromCache, fetchFromDB, fetchFromAPI]) - -Effect.runPromise(raceAll).then((result) => { - console.log(`Winner: ${result.source}`) // "cache" -}) - -// ============================================ -// PRACTICAL: API with timeout and fallback -// ============================================ - -const fetchWithResilience = (url: string) => - Effect.gen(function* () { - const response = yield* Effect.tryPromise(() => - fetch(url).then((r) => r.json()) - ).pipe( - Effect.timeout("3 seconds"), - Effect.flatMap((opt) => - Option.isSome(opt) - ? Effect.succeed(opt.value) - : Effect.succeed({ error: "timeout", cached: true }) - ) - ) - - return response - }) -``` - -### Explanation - -Racing and timeouts prevent your app from hanging: - -1. **Redundant requests** - Race multiple servers, use fastest response -2. **Timeouts** - Fail fast if operation takes too long -3. **Fallbacks** - Try fast path, fall back to slow path - ---- - ---- - -## Read Effect Type Errors - -**Rule:** Effect errors are verbose but structured - learn to extract the key information. - -**Skill Level:** beginner - -**Use Cases:** tooling-and-debugging - -### Explanation - -Effect's type system catches many bugs at compile time, but: - -1. **Effect types are complex** - Three type parameters -2. **Errors are nested** - Multiple layers of generics -3. **Messages are verbose** - TypeScript shows everything - -Understanding the pattern makes errors manageable. - ---- - ---- - -## Redact and Handle Sensitive Data - -**Rule:** Use Redacted to wrap sensitive values, preventing accidental exposure in logs or error messages. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Redacted } from "effect"; - -// Wrap a sensitive value -const secret = Redacted.make("super-secret-password"); - -// Use the secret in your application logic -function authenticate(user: string, password: Redacted.Redacted) { - // ... authentication logic -} - -// Logging or stringifying a Redacted value -console.log(`Password: ${secret}`); // Output: Password: -console.log(String(secret)); // Output: -``` - -**Explanation:** - -- `Redacted.make(value)` wraps a sensitive value. -- When logged or stringified, the value is replaced with ``. -- Prevents accidental exposure of secrets in logs or error messages. - -### Anti-Pattern - -Passing sensitive data as plain strings, which can be accidentally logged, serialized, or leaked in error messages. - -### Explanation - -Sensitive data should never appear in logs, traces, or error messages. -`Redacted` provides a type-safe way to mark and protect secrets throughout your application. - ---- - -## Representing Time Spans with Duration - -**Rule:** Use Duration to model and manipulate time spans, enabling safe and expressive time-based logic. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Duration } from "effect"; - -// Create durations using helpers -const oneSecond = Duration.seconds(1); -const fiveMinutes = Duration.minutes(5); -const twoHours = Duration.hours(2); - -// Add, subtract, and compare durations -const total = Duration.sum(oneSecond, fiveMinutes); // 5 min 1 sec -const isLonger = Duration.greaterThan(twoHours, fiveMinutes); // true - -// Convert to milliseconds or ISO string -const ms = Duration.toMillis(fiveMinutes); // 300000 -const iso = Duration.formatIso(oneSecond); // "PT1S" -``` - -**Explanation:** - -- `Duration` is immutable and type-safe. -- Use helpers for common intervals and arithmetic for composition. -- Prefer `Duration` over raw numbers for all time-based logic. - -### Anti-Pattern - -Using raw numbers (e.g., `5000` for 5 seconds) for time intervals, which is error-prone, hard to read, and less maintainable. - -### Explanation - -Working with raw numbers for time intervals (e.g., milliseconds) is error-prone and hard to read. -`Duration` provides a clear, expressive API for modeling time spans, improving code safety and maintainability. - ---- - -## Representing Time Spans with Duration - -**Rule:** Use the Duration data type to represent time intervals instead of raw numbers. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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); -``` - ---- - -### Anti-Pattern - -Using raw numbers for time-based operations. This is ambiguous and error-prone. - -```typescript -import { Effect } from "effect"; - -// ❌ WRONG: What does '2000' mean? Milliseconds? Seconds? -const program = Effect.log("Waiting...").pipe(Effect.delay(2000)); - -// This is especially dangerous when different parts of an application -// use different conventions (e.g., one service uses seconds, another uses milliseconds). -// Using Duration eliminates this entire class of bugs. -``` - -### Explanation - -Using raw numbers to represent time is a common source of bugs and confusion. When you see `setTimeout(fn, 5000)`, it's not immediately clear if the unit is seconds or milliseconds without prior knowledge of the API. - -`Duration` solves this by making the unit explicit in the code. It provides a type-safe, immutable, and human-readable way to work with time intervals. This eliminates ambiguity and makes your code easier to read and maintain. Durations are used throughout Effect's time-based operators, such as `Effect.sleep`, `Effect.timeout`, and `Schedule`. - ---- - ---- - -## Retry a Failed Operation with Effect.retry - -**Rule:** Retry failed operations with Effect.retry. - -**Skill Level:** beginner - -**Use Cases:** getting-started - -### Good Example - -```typescript -import { Effect, Schedule, pipe } from "effect"; - -class ApiError { - readonly _tag = "ApiError"; - constructor(readonly status: number) {} -} - -const fetchUserData = (userId: string) => - Effect.tryPromise({ - try: async () => { - const response = await fetch(`/api/users/${userId}`); - if (!response.ok) throw new ApiError(response.status); - return response.json(); - }, - catch: (error) => error as ApiError, - }); - -// Retry up to 3 times with 500ms between attempts -const fetchWithRetry = (userId: string) => - pipe( - fetchUserData(userId), - Effect.retry( - Schedule.recurs(3).pipe(Schedule.addDelay(() => "500 millis")) - ), - Effect.catchAll((error) => - Effect.succeed({ error: `Failed after retries: ${error._tag}` }) - ) - ); -``` - -### Explanation - -Network requests fail. Databases time out. Services go down temporarily. -Instead of failing immediately, you often want to retry a few times. -Effect makes this a one-liner. - ---- - -## Retry Failed Operations - -**Rule:** Use Effect.retry with a Schedule to handle transient failures gracefully. - -**Skill Level:** beginner - -**Use Cases:** scheduling - -### Good Example - -```typescript -import { Effect, Schedule, Data } from "effect" - -// ============================================ -// 1. Define error types -// ============================================ - -class NetworkError extends Data.TaggedError("NetworkError")<{ - readonly message: string -}> {} - -class RateLimitError extends Data.TaggedError("RateLimitError")<{ - readonly retryAfter: number -}> {} - -class NotFoundError extends Data.TaggedError("NotFoundError")<{ - readonly resource: string -}> {} - -// ============================================ -// 2. Simulate a flaky API call -// ============================================ - -let callCount = 0 -const fetchData = Effect.gen(function* () { - callCount++ - yield* Effect.log(`API call attempt ${callCount}`) - - // Simulate intermittent failures - if (callCount < 3) { - return yield* Effect.fail(new NetworkError({ message: "Connection timeout" })) - } - - return { data: "Success!", attempts: callCount } -}) - -// ============================================ -// 3. Basic retry - fixed attempts -// ============================================ - -const withBasicRetry = fetchData.pipe( - Effect.retry(Schedule.recurs(5)) // Retry up to 5 times -) - -// ============================================ -// 4. Retry with delay -// ============================================ - -const withDelayedRetry = fetchData.pipe( - Effect.retry( - Schedule.spaced("500 millis").pipe( - Schedule.intersect(Schedule.recurs(5)) - ) - ) -) - -// ============================================ -// 5. Retry only specific errors -// ============================================ - -const fetchWithErrors = (shouldFail: boolean) => - Effect.gen(function* () { - if (shouldFail) { - // Randomly fail with different errors - const random = Math.random() - if (random < 0.5) { - return yield* Effect.fail(new NetworkError({ message: "Timeout" })) - } else if (random < 0.8) { - return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 })) - } else { - return yield* Effect.fail(new NotFoundError({ resource: "user:123" })) - } - } - return "Data fetched!" - }) - -// Only retry network and rate limit errors, not NotFoundError -const retryTransientOnly = fetchWithErrors(true).pipe( - Effect.retry({ - schedule: Schedule.recurs(3), - while: (error) => - error._tag === "NetworkError" || error._tag === "RateLimitError", - }) -) - -// ============================================ -// 6. Retry with exponential backoff -// ============================================ - -const withExponentialBackoff = fetchData.pipe( - Effect.retry( - Schedule.exponential("100 millis", 2).pipe( // 100ms, 200ms, 400ms... - Schedule.intersect(Schedule.recurs(5)) // Max 5 retries - ) - ) -) - -// ============================================ -// 7. Run and observe -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("Starting retry demo...") - - // Reset counter - callCount = 0 - - const result = yield* withBasicRetry - yield* Effect.log(`Final result: ${JSON.stringify(result)}`) -}) - -Effect.runPromise(program) -``` - -### Explanation - -Many failures are temporary: - -1. **Network issues** - Connection drops, timeouts -2. **Rate limits** - Too many requests -3. **Resource contention** - Database locks -4. **Service restarts** - Brief unavailability - -Automatic retries handle these without manual intervention. - ---- - ---- - -## Retry HTTP Requests with Backoff - -**Rule:** Use Schedule to retry failed HTTP requests with configurable backoff strategies. - -**Skill Level:** intermediate - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Schedule, Duration, Data } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse, HttpClientError } from "@effect/platform" - -// ============================================ -// 1. Basic retry with exponential backoff -// ============================================ - -const fetchWithRetry = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((response) => HttpClientResponse.json(response)), - Effect.retry( - Schedule.exponential("100 millis", 2).pipe( - Schedule.intersect(Schedule.recurs(5)), // Max 5 retries - Schedule.jittered // Add randomness - ) - ) - ) - }) - -// ============================================ -// 2. Retry only specific status codes -// ============================================ - -class RetryableHttpError extends Data.TaggedError("RetryableHttpError")<{ - readonly status: number - readonly message: string -}> {} - -class NonRetryableHttpError extends Data.TaggedError("NonRetryableHttpError")<{ - readonly status: number - readonly message: string -}> {} - -const isRetryable = (status: number): boolean => - status === 429 || // Rate limited - status === 503 || // Service unavailable - status === 502 || // Bad gateway - status === 504 || // Gateway timeout - status >= 500 // Server errors - -const fetchWithSelectiveRetry = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const response = yield* client.get(url).pipe( - Effect.flatMap((response) => { - if (response.status >= 400) { - if (isRetryable(response.status)) { - return Effect.fail(new RetryableHttpError({ - status: response.status, - message: `HTTP ${response.status}`, - })) - } - return Effect.fail(new NonRetryableHttpError({ - status: response.status, - message: `HTTP ${response.status}`, - })) - } - return Effect.succeed(response) - }), - Effect.retry({ - schedule: Schedule.exponential("200 millis").pipe( - Schedule.intersect(Schedule.recurs(3)) - ), - while: (error) => error._tag === "RetryableHttpError", - }) - ) - - return yield* HttpClientResponse.json(response) - }) - -// ============================================ -// 3. Retry with logging -// ============================================ - -const fetchWithRetryLogging = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - return yield* client.get(url).pipe( - Effect.flatMap((r) => HttpClientResponse.json(r)), - Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.tapOutput((_, output) => - Effect.log(`Retry attempt, waiting ${Duration.toMillis(output)}ms`) - ) - ) - ), - Effect.tapError((error) => Effect.log(`Request failed: ${error}`)) - ) - }) - -// ============================================ -// 4. Custom retry policy -// ============================================ - -const customRetryPolicy = Schedule.exponential("500 millis", 2).pipe( - Schedule.intersect(Schedule.recurs(5)), - Schedule.union(Schedule.spaced("30 seconds")), // Also retry after 30s - Schedule.whileOutput((duration) => Duration.lessThanOrEqualTo(duration, "2 minutes")), - Schedule.jittered -) - -// ============================================ -// 5. Retry respecting Retry-After header -// ============================================ - -const fetchWithRetryAfter = (url: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const makeRequest = client.get(url).pipe( - Effect.flatMap((response) => { - if (response.status === 429) { - const retryAfter = response.headers["retry-after"] - const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000 - - return Effect.fail({ - _tag: "RateLimited" as const, - delay, - }) - } - return Effect.succeed(response) - }) - ) - - return yield* makeRequest.pipe( - Effect.retry( - Schedule.recurWhile<{ _tag: "RateLimited"; delay: number }>( - (error) => error._tag === "RateLimited" - ).pipe( - Schedule.intersect(Schedule.recurs(3)), - Schedule.delayed((_, error) => Duration.millis(error.delay)) - ) - ), - Effect.flatMap((r) => HttpClientResponse.json(r)) - ) - }) - -// ============================================ -// 6. Usage -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("Fetching with retry...") - - const data = yield* fetchWithRetry("https://api.example.com/data").pipe( - Effect.catchAll((error) => { - return Effect.succeed({ error: "All retries exhausted" }) - }) - ) - - yield* Effect.log(`Result: ${JSON.stringify(data)}`) -}) -``` - -### Explanation - -HTTP requests fail for transient reasons: - -1. **Network issues** - Temporary connectivity problems -2. **Server overload** - 503 Service Unavailable -3. **Rate limits** - 429 Too Many Requests -4. **Timeouts** - Slow responses - -Proper retry logic handles these gracefully. - ---- - ---- - -## Retry Operations Based on Specific Errors - -**Rule:** Use predicate-based retry policies to retry an operation only for specific, recoverable errors. - -**Skill Level:** intermediate - -**Use Cases:** error-management - -### Good 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 { Data, Effect, Schedule } 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))); -``` - ---- - -### Anti-Pattern - -Using a generic `Effect.retry` that retries on all errors. This can lead to wasted resources and obscure permanent issues. - -```typescript -import { Effect, Schedule } from "effect"; -import { flakyApiCall } from "./somewhere"; // From previous example - -// ❌ WRONG: This policy will retry even if the API returns a 404 Not Found. -// This wastes time and network requests on an error that will never succeed. -const blindRetryPolicy = Schedule.recurs(3); - -const program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy)); -``` - -### Explanation - -Not all errors are created equal. Retrying on a permanent error like "permission denied" or "not found" is pointless and can hide underlying issues. You only want to retry on _transient_, recoverable errors, such as network timeouts or "server busy" responses. - -By adding a predicate to your retry schedule, you gain fine-grained control over the retry logic. This allows you to build much more intelligent and efficient error handling systems that react appropriately to different failure modes. This is a common requirement for building robust clients for external APIs. - ---- - ---- - -## Run a Pipeline for its Side Effects - -**Rule:** Use Stream.runDrain to execute a stream for its side effects when you don't need the final values. - -**Skill Level:** beginner - -**Use Cases:** building-data-pipelines - -### Good Example - -This example creates a stream of tasks. For each task, it performs a side effect (logging it as "complete"). `Stream.runDrain` executes the pipeline, ensuring all logs are written, but without collecting the `void` results of each logging operation. - -```typescript -import { Effect, Stream } from "effect"; - -const tasks = ["task 1", "task 2", "task 3"]; - -// A function that performs a side effect for a task -const completeTask = (task: string): Effect.Effect => - Effect.log(`Completing ${task}`); - -const program = Stream.fromIterable(tasks).pipe( - // For each task, run the side-effectful operation - Stream.mapEffect(completeTask, { concurrency: 1 }), - // Run the stream for its effects, discarding the `void` results - Stream.runDrain -); - -const programWithLogging = Effect.gen(function* () { - yield* program; - yield* Effect.log("\nAll tasks have been processed."); -}); - -Effect.runPromise(programWithLogging); -/* -Output: -... level=INFO msg="Completing task 1" -... level=INFO msg="Completing task 2" -... level=INFO msg="Completing task 3" - -All tasks have been processed. -*/ -``` - -### Anti-Pattern - -The anti-pattern is using `Stream.runCollect` when you only care about the side effects. This needlessly consumes memory and can lead to crashes. - -```typescript -import { Effect, Stream } from "effect"; -// ... same tasks and completeTask function ... - -const program = Stream.fromIterable(tasks).pipe( - Stream.mapEffect(completeTask, { concurrency: 1 }), - // Anti-pattern: Collecting results that we are just going to ignore - Stream.runCollect -); - -Effect.runPromise(program).then((results) => { - // The `results` variable here is a Chunk of `[void, void, void]`. - // It served no purpose but consumed memory. - console.log( - `\nAll tasks processed. Unnecessarily collected ${results.length} empty results.` - ); -}); -``` - -While this works for a small array of three items, it's a dangerous habit. If the `tasks` array contained millions of items, this code would create a `Chunk` with millions of `void` values, consuming a significant amount of memory for no reason and potentially crashing the application. `Stream.runDrain` avoids this problem entirely. - -### Explanation - -Not all pipelines are designed to produce a final list of values. Often, the goal is to perform an action for each item—write it to a database, send it to a message queue, or log it to a file. In these "fire and forget" scenarios, collecting the results is not just unnecessary; it's a performance anti-pattern. - -`Stream.runDrain` is the perfect tool for this job: - -1. **Memory Efficiency**: This is its primary advantage. `runDrain` processes each item and then immediately discards it, resulting in constant, minimal memory usage. This makes it the only safe choice for processing extremely large or infinite streams. -2. **Clarity of Intent**: Using `runDrain` clearly communicates that you are interested in the successful execution of the stream's effects, not in its output values. The final `Effect` it produces resolves to `void`, reinforcing that no value is returned. -3. **Performance**: By avoiding the overhead of allocating and managing a growing list in memory, `runDrain` can be faster for pipelines with a very large number of small items. - ---- - ---- - -## Run Background Tasks with Effect.fork - -**Rule:** Use Effect.fork to start a non-blocking background process and manage its lifecycle via its Fiber. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This program forks a background process that logs a "tick" every second. The main process does its own work for 5 seconds and then explicitly interrupts the background logger before exiting. - -```typescript -import { Effect, Fiber } from "effect"; - -// A long-running effect that logs a message every second, forever -// Effect.forever creates an infinite loop that repeats the effect -// This simulates a background service like a health check or monitoring task -const tickingClock = Effect.log("tick").pipe( - Effect.delay("1 second"), // Wait 1 second between ticks - Effect.forever // Repeat indefinitely - this creates an infinite effect -); - -const program = Effect.gen(function* () { - yield* Effect.log("Forking the ticking clock into the background."); - - // Start the clock, but don't wait for it. - // Effect.fork creates a new fiber that runs concurrently with the main program - // The main fiber continues immediately without waiting for the background task - // This is essential for non-blocking background operations - const clockFiber = yield* Effect.fork(tickingClock); - - // At this point, we have two fibers running: - // 1. The main fiber (this program) - // 2. The background clock fiber (ticking every second) - - yield* Effect.log("Main process is now doing other work for 5 seconds..."); - - // Simulate the main application doing work - // While this sleep happens, the background clock continues ticking - // This demonstrates true concurrency - both fibers run simultaneously - yield* Effect.sleep("5 seconds"); - - yield* Effect.log("Main process is done. Interrupting the clock fiber."); - - // Stop the background process. - // Fiber.interrupt sends an interruption signal to the fiber - // This allows the fiber to perform cleanup operations before terminating - // Without this, the background task would continue running indefinitely - yield* Fiber.interrupt(clockFiber); - - // Important: Always clean up background fibers to prevent resource leaks - // In a real application, you might want to: - // 1. Use Fiber.join instead of interrupt to wait for graceful completion - // 2. Handle interruption signals within the background task - // 3. Implement proper shutdown procedures - - yield* Effect.log("Program finished."); - - // Key concepts demonstrated: - // 1. Fork creates concurrent fibers without blocking - // 2. Background tasks run independently of the main program - // 3. Fiber interruption provides controlled shutdown - // 4. Multiple fibers can run simultaneously on the same thread pool -}); - -// This example shows how to: -// - Run background tasks that don't block the main program -// - Manage fiber lifecycles (create, run, interrupt) -// - Coordinate between multiple concurrent operations -// - Properly clean up resources when shutting down -Effect.runPromise(program); -``` - ---- - -### Anti-Pattern - -The anti-pattern is using `Effect.fork` when you immediately need the result of the computation. This is an overly complicated and less readable way of just running the effect directly. - -```typescript -import { Effect, Fiber } from "effect"; - -const someEffect = Effect.succeed(42); - -// ❌ WRONG: This is unnecessarily complex. -const program = Effect.gen(function* () { - const fiber = yield* Effect.fork(someEffect); - // You immediately wait for the result, defeating the purpose of forking. - const result = yield* Fiber.join(fiber); - return result; -}); - -// ✅ CORRECT: Just run the effect directly if you need its result right away. -const simplerProgram = Effect.gen(function* () { - const result = yield* someEffect; - return result; -}); -``` - -### Explanation - -Unlike `Effect.all` or a direct `yield*`, which wait for the computation to complete, `Effect.fork` is a "fire and forget" operation. It starts the effect on a new, concurrent fiber and immediately returns control to the parent fiber. - -This is essential for managing long-running background tasks like: - -- A web server listener. -- A message queue consumer. -- A periodic cache cleanup job. - -The returned `Fiber` object is your remote control for the background task. You can use `Fiber.interrupt` to safely stop it (ensuring all its finalizers are run) or `Fiber.join` to wait for it to complete at some later point. - ---- - ---- - -## Run Independent Effects in Parallel with Effect.all - -**Rule:** Use Effect.all to execute a collection of independent effects concurrently. - -**Skill Level:** intermediate - -**Use Cases:** concurrency - -### Good 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 - must specify concurrency option! -const program = Effect.all([fetchUser, fetchPosts], { - concurrency: "unbounded", -}); - -// 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). -const programWithLogging = Effect.gen(function* () { - const results = yield* program; - yield* Effect.log(`Results: ${JSON.stringify(results)}`); - return results; -}); - -Effect.runPromise(programWithLogging); -``` - ---- - -### Anti-Pattern - -The anti-pattern is running independent tasks sequentially using `Effect.gen`. This is inefficient and unnecessarily slows down your application. - -```typescript -import { Effect } from "effect"; -import { fetchUser, fetchPosts } from "./somewhere"; // From previous example - -// ❌ WRONG: This is inefficient. -const program = Effect.gen(function* () { - // fetchUser runs and completes... - const user = yield* fetchUser; - // ...only then does fetchPosts begin. - const posts = yield* fetchPosts; - return [user, posts]; -}); - -// Total execution time will be ~2.5 seconds (1s + 1.5s), -// which is a full second slower than the parallel version. -Effect.runPromise(program).then(console.log); -``` - -### Explanation - -Running tasks sequentially when they could be done in parallel is a common source of performance bottlenecks. `Effect.all` is the solution. It's the direct equivalent of `Promise.all` in the Effect ecosystem. - -Instead of waiting for Task A to finish before starting Task B, `Effect.all` starts all tasks simultaneously. The total time to complete is determined by the duration of the _longest_ running effect, not the sum of all durations. If any single effect in the collection fails, the entire `Effect.all` will fail immediately. - ---- - ---- - -## Run Multiple Effects in Parallel with Effect.all - -**Rule:** Run multiple Effects in parallel with Effect.all. - -**Skill Level:** beginner - -**Use Cases:** getting-started - -### Good Example - -```typescript -import { Effect, pipe } from "effect"; - -// Simulate fetching data from different sources -const fetchUser = Effect.succeed({ id: 1, name: "Alice" }).pipe( - Effect.delay("100 millis") -); - -const fetchPosts = Effect.succeed([ - { id: 1, title: "Hello World" }, - { id: 2, title: "Effect is awesome" }, -]).pipe(Effect.delay("150 millis")); - -const fetchSettings = Effect.succeed({ theme: "dark" }).pipe( - Effect.delay("50 millis") -); - -// Fetch all data in parallel -const program = Effect.gen(function* () { - const [user, posts, settings] = yield* Effect.all( - [fetchUser, fetchPosts, fetchSettings], - { concurrency: "unbounded" } - ); - - yield* Effect.log(`Loaded ${user.name} with ${posts.length} posts`); - return { user, posts, settings }; -}); - -Effect.runPromise(program); -``` - -### Explanation - -Real applications often need to do multiple things at once - fetch data from -several APIs, process multiple files, etc. `Effect.all` lets you express -this naturally without callback hell or complex Promise.all patterns. - ---- - -## Running and Collecting Stream Results - -**Rule:** Choose the right Stream.run* method based on what you need from the results. - -**Skill Level:** beginner - -**Use Cases:** streams-getting-started - -### Good Example - -```typescript -import { Effect, Stream, Option } from "effect" - -const numbers = Stream.make(1, 2, 3, 4, 5) - -// ============================================ -// runCollect - Get all results as a Chunk -// ============================================ - -const collectAll = numbers.pipe( - Stream.map((n) => n * 10), - Stream.runCollect -) - -Effect.runPromise(collectAll).then((chunk) => { - console.log([...chunk]) // [10, 20, 30, 40, 50] -}) - -// ============================================ -// runForEach - Process each item -// ============================================ - -const processEach = numbers.pipe( - Stream.runForEach((n) => - Effect.log(`Processing: ${n}`) - ) -) - -Effect.runPromise(processEach) -// Logs: Processing: 1, Processing: 2, etc. - -// ============================================ -// runDrain - Run for side effects only -// ============================================ - -const withSideEffects = numbers.pipe( - Stream.tap((n) => Effect.log(`Saw: ${n}`)), - Stream.runDrain // Discard values, just run -) - -// ============================================ -// runHead - Get first value only -// ============================================ - -const getFirst = numbers.pipe( - Stream.runHead -) - -Effect.runPromise(getFirst).then((option) => { - if (Option.isSome(option)) { - console.log(`First: ${option.value}`) // First: 1 - } -}) - -// ============================================ -// runLast - Get last value only -// ============================================ - -const getLast = numbers.pipe( - Stream.runLast -) - -Effect.runPromise(getLast).then((option) => { - if (Option.isSome(option)) { - console.log(`Last: ${option.value}`) // Last: 5 - } -}) - -// ============================================ -// runFold - Accumulate into single result -// ============================================ - -const sum = numbers.pipe( - Stream.runFold(0, (acc, n) => acc + n) -) - -Effect.runPromise(sum).then((total) => { - console.log(`Sum: ${total}`) // Sum: 15 -}) - -// ============================================ -// runCount - Count elements -// ============================================ - -const count = numbers.pipe(Stream.runCount) - -Effect.runPromise(count).then((n) => { - console.log(`Count: ${n}`) // Count: 5 -}) -``` - -### Explanation - -Effect provides several ways to consume a stream, each optimized for different use cases: - -| Method | Returns | Use When | -|--------|---------|----------| -| **runCollect** | `Chunk` | Need all results in memory | -| **runForEach** | `void` | Process each item for side effects | -| **runDrain** | `void` | Run for side effects, ignore values | -| **runHead** | `Option` | Only need first value | -| **runLast** | `Option` | Only need last value | -| **runFold** | `S` | Accumulate into single result | - ---- - ---- - -## Safely Bracket Resource Usage with `acquireRelease` - -**Rule:** Bracket the use of a resource between an `acquire` and a `release` effect. - -**Skill Level:** beginner - -**Use Cases:** resource-management - -### Good Example - -```typescript -import { Effect, Console } from "effect"; - -// A mock resource that needs to be managed -const getDbConnection = Effect.sync(() => ({ id: Math.random() })).pipe( - Effect.tap(() => Effect.log("Connection Acquired")) -); - -const closeDbConnection = (conn: { - id: number; -}): Effect.Effect => - Effect.log(`Connection ${conn.id} Released`); - -// The program that uses the resource -const program = Effect.acquireRelease( - getDbConnection, // 1. acquire - (connection) => closeDbConnection(connection) // 2. cleanup -).pipe( - Effect.tap((connection) => - Effect.log(`Using connection ${connection.id} to run query...`) - ) -); - -Effect.runPromise(Effect.scoped(program)); - -/* -Output: -Connection Acquired -Using connection 0.12345... to run query... -Connection 0.12345... Released -*/ -``` - -**Explanation:** -By using `Effect.acquireRelease`, the `closeDbConnection` logic is guaranteed to run after the main logic completes. This creates a self-contained, leak-proof unit of work that can be safely composed into larger programs. - -### Anti-Pattern - -Using a standard `try...finally` block with `async/await`. While it handles success and failure cases, it is **not interruption-safe**. If the fiber executing the `Promise` is interrupted by Effect's structured concurrency, the `finally` block is not guaranteed to run, leading to resource leaks. - -```typescript -// ANTI-PATTERN: Not interruption-safe -async function getUser() { - const connection = await getDbConnectionPromise(); // acquire - try { - return await useConnectionPromise(connection); // use - } finally { - // This block may not run if the fiber is interrupted! - await closeConnectionPromise(connection); // release - } -} -``` - -### Explanation - -This pattern is the foundation of resource safety in Effect. It provides a composable and interruption-safe alternative to a standard `try...finally` block. The `release` effect is guaranteed to execute, preventing resource leaks which are common in complex asynchronous applications, especially those involving concurrency where tasks can be cancelled. - ---- - -## Scheduling Pattern 1: Repeat an Effect on a Fixed Interval - -**Rule:** Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks. - -**Skill Level:** intermediate - -**Use Cases:** scheduling - -### Good Example - -This example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status. - -```typescript -import { Effect, Schedule, Duration } from "effect"; - -interface ServiceStatus { - readonly service: string; - readonly url: string; - readonly isHealthy: boolean; - readonly responseTime: number; - readonly lastChecked: number; -} - -// Mock health check that calls an endpoint -const checkServiceHealth = ( - url: string, - service: string -): Effect.Effect => - Effect.gen(function* () { - const startTime = Date.now(); - - // Simulate HTTP call with occasional failures - const isHealthy = Math.random() > 0.1; // 90% success rate - const responseTime = Math.random() * 500; // 0-500ms - - yield* Effect.sleep(Duration.millis(Math.round(responseTime))); - - if (!isHealthy) { - yield* Effect.fail(new Error(`${service} is unhealthy`)); - } - - return { - service, - url, - isHealthy: true, - responseTime: Math.round(Date.now() - startTime), - lastChecked: Date.now(), - }; - }); - -// Health check for multiple services -interface HealthCheckConfig { - readonly services: Array<{ - readonly name: string; - readonly url: string; - }>; - readonly intervalSeconds: number; -} - -// Keep track of service status -const serviceStatuses = new Map(); - -// Check all services and report status -const checkAllServices = ( - config: HealthCheckConfig -): Effect.Effect => - Effect.gen(function* () { - for (const service of config.services) { - const status = yield* checkServiceHealth(service.url, service.name).pipe( - Effect.either - ); - - if (status._tag === "Right") { - serviceStatuses.set(service.name, status.right); - console.log( - `✓ ${service.name}: OK (${status.right.responseTime}ms)` - ); - } else { - console.log(`✗ ${service.name}: FAILED`); - // Keep last known status if available - } - } - }); - -// Create the repeating health check -const createHealthCheckScheduler = ( - config: HealthCheckConfig -): Effect.Effect => - checkAllServices(config).pipe( - // Schedule with fixed interval (fixed = ignore execution time) - Effect.repeat( - Schedule.fixed(Duration.seconds(config.intervalSeconds)) - ) - ); - -// Report current status -const reportStatus = (): Effect.Effect => - Effect.sync(() => { - if (serviceStatuses.size === 0) { - console.log("\n[STATUS] No services checked yet"); - return; - } - - console.log("\n[STATUS REPORT]"); - for (const [service, status] of serviceStatuses) { - const ago = Math.round((Date.now() - status.lastChecked) / 1000); - console.log( - ` ${service}: ${status.isHealthy ? "✓" : "✗"} (checked ${ago}s ago)` - ); - } - }); - -// Run health checker in background and check status periodically -const program = Effect.gen(function* () { - const config: HealthCheckConfig = { - services: [ - { name: "API", url: "https://api.example.com/health" }, - { name: "Database", url: "https://db.example.com/health" }, - { name: "Cache", url: "https://cache.example.com/health" }, - ], - intervalSeconds: 5, // Check every 5 seconds - }; - - // Fork the health checker to run in background - const checker = yield* createHealthCheckScheduler(config).pipe( - Effect.fork - ); - - // Check and report status every 15 seconds for 60 seconds - yield* reportStatus().pipe( - Effect.repeat( - Schedule.addDelay( - Schedule.recurs(3), // 3 repetitions = 4 total (initial + 3) - () => Duration.seconds(15) - ) - ) - ); - - // Interrupt the background checker - yield* checker.interrupt(); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Defines service health checks** that may fail -2. **Uses Schedule.fixed** to repeat every 5 seconds -3. **Handles failures gracefully** (keeps last known status) -4. **Runs in background** while main logic continues -5. **Reports current status** at intervals - ---- - -### Explanation - -Many production systems need periodic operations: - -- **Health checks**: Poll service availability every 30 seconds -- **Cache refresh**: Update cache every 5 minutes -- **Metrics collection**: Gather system metrics every 10 seconds -- **Data sync**: Sync data with remote service periodically -- **Cleanup tasks**: Remove stale data nightly - -Without proper scheduling: - -- Manual polling with `while` loops wastes CPU (busy-waiting) -- Thread.sleep blocks threads, preventing other work -- No automatic restart on failure -- Difficult to test deterministically - -With `Schedule.fixed`: - -- Efficient, non-blocking repetition -- Automatic failure handling and retry -- Testable with TestClock -- Clean, declarative syntax - ---- - ---- - -## Scheduling Pattern 2: Implement Exponential Backoff for Retries - -**Rule:** Use exponential backoff with jitter for retries to prevent overwhelming failing services and improve success likelihood through smart timing. - -**Skill Level:** intermediate - -**Use Cases:** error-handling-resilience - -### Good Example - -This example demonstrates exponential backoff with jitter for retrying a flaky API call. - -```typescript -import { Effect, Schedule } from "effect"; - -interface RetryStats { - readonly attempt: number; - readonly delay: number; - readonly lastError?: Error; -} - -// Simulate flaky API that fails first 3 times, succeeds on 4th -let attemptCount = 0; - -const flakyApiCall = (): Effect.Effect<{ status: string }> => - Effect.gen(function* () { - attemptCount++; - yield* Effect.log(`[API] Attempt ${attemptCount}`); - - if (attemptCount < 4) { - yield* Effect.fail(new Error("Service temporarily unavailable (503)")); - } - - return { status: "ok" }; - }); - -// Calculate exponential backoff with jitter -interface BackoffConfig { - readonly baseDelayMs: number; - readonly maxDelayMs: number; - readonly maxRetries: number; -} - -const exponentialBackoffWithJitter = (config: BackoffConfig) => { - let attempt = 0; - - // Calculate delay for this attempt - const calculateDelay = (): number => { - const exponential = config.baseDelayMs * Math.pow(2, attempt); - const withJitter = exponential * (0.5 + Math.random() * 0.5); // ±50% jitter - const capped = Math.min(withJitter, config.maxDelayMs); - - yield* Effect.log( - `[BACKOFF] Attempt ${attempt + 1}: ${Math.round(capped)}ms delay` - ); - - return Math.round(capped); - }; - - return Effect.gen(function* () { - const effect = flakyApiCall(); - - let lastError: Error | undefined; - - for (attempt = 0; attempt < config.maxRetries; attempt++) { - const result = yield* effect.pipe(Effect.either); - - if (result._tag === "Right") { - yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attempt + 1}`); - return result.right; - } - - lastError = result.left; - - if (attempt < config.maxRetries - 1) { - const delay = calculateDelay(); - yield* Effect.sleep(`${delay} millis`); - } - } - - yield* Effect.log( - `[FAILURE] All ${config.maxRetries} attempts exhausted` - ); - yield* Effect.fail(lastError); - }); -}; - -// Run with exponential backoff -const program = exponentialBackoffWithJitter({ - baseDelayMs: 100, - maxDelayMs: 5000, - maxRetries: 5, -}); - -console.log( - `\n[START] Retrying flaky API with exponential backoff\n` -); - -Effect.runPromise(program).then( - (result) => console.log(`\n[RESULT] ${JSON.stringify(result)}\n`), - (error) => console.error(`\n[ERROR] ${error.message}\n`) -); -``` - -Output demonstrates increasing delays with jitter: -``` -[START] Retrying flaky API with exponential backoff - -[API] Attempt 1 -[BACKOFF] Attempt 1: 78ms delay -[API] Attempt 2 -[BACKOFF] Attempt 2: 192ms delay -[API] Attempt 3 -[BACKOFF] Attempt 3: 356ms delay -[API] Attempt 4 -[SUCCESS] Succeeded on attempt 4 - -[RESULT] {"status":"ok"} -``` - ---- - -### Explanation - -Naive retry strategies fail under load: - -**Immediate retry**: -- All failures retry at once -- Fails service under load (recovery takes longer) -- Leads to cascade failure - -**Fixed backoff** (e.g., 1 second always): -- No pressure reduction during recovery -- Multiple clients cause thundering herd -- Predictable = synchronized retries - -**Exponential backoff**: -- Gives failing service time to recover -- Each retry waits progressively longer -- Without jitter, synchronized retries still hammer service - -**Exponential backoff + jitter**: -- Spreads retry attempts over time -- Failures de-correlate across clients -- Service recovery time properly utilized -- Success likelihood increases with each retry - -Real-world example: 100 clients fail simultaneously -- **Immediate retry**: 100 requests in milliseconds → failure -- **Fixed backoff**: 100 requests at exactly 1s → failure -- **Exponential**: 100 requests at 100ms, 200ms, 400ms, 800ms → recovery → success - ---- - ---- - -## Scheduling Pattern 3: Schedule Tasks with Cron Expressions - -**Rule:** Use cron expressions to schedule periodic tasks at specific calendar times, enabling flexible scheduling beyond simple fixed intervals. - -**Skill Level:** intermediate - -**Use Cases:** scheduling-periodic-tasks - -### Good Example - -This example demonstrates scheduling a daily report generation using cron, with timezone support. - -```typescript -import { Effect, Schedule, Console } from "effect"; -import { DateTime } from "luxon"; // For timezone handling - -interface ReportConfig { - readonly cronExpression: string; - readonly timezone?: string; - readonly jobName: string; -} - -interface ScheduledReport { - readonly timestamp: Date; - readonly jobName: string; - readonly result: string; -} - -// Simple cron parser (in production, use a library like cron-parser) -const parseCronExpression = ( - expression: string -): { - minute: number[]; - hour: number[]; - dayOfMonth: number[]; - month: number[]; - dayOfWeek: number[]; -} => { - const parts = expression.split(" "); - - const parseField = (field: string, max: number): number[] => { - if (field === "*") { - return Array.from({ length: max + 1 }, (_, i) => i); - } - - if (field.includes(",")) { - return field.split(",").flatMap((part) => parseField(part, max)); - } - - if (field.includes("-")) { - const [start, end] = field.split("-").map(Number); - return Array.from({ length: end - start + 1 }, (_, i) => start + i); - } - - return [Number(field)]; - }; - - return { - minute: parseField(parts[0], 59), - hour: parseField(parts[1], 23), - dayOfMonth: parseField(parts[2], 31), - month: parseField(parts[3], 12), - dayOfWeek: parseField(parts[4], 6), - }; -}; - -// Check if current time matches cron expression -const shouldRunNow = (parsed: ReturnType): boolean => { - const now = new Date(); - - return ( - parsed.minute.includes(now.getUTCMinutes()) && - parsed.hour.includes(now.getUTCHours()) && - parsed.dayOfMonth.includes(now.getUTCDate()) && - parsed.month.includes(now.getUTCMonth() + 1) && - parsed.dayOfWeek.includes(now.getUTCDay()) - ); -}; - -// Generate a report -const generateReport = (jobName: string): Effect.Effect => - Effect.gen(function* () { - yield* Console.log(`[REPORT] Generating ${jobName}...`); - - // Simulate report generation - yield* Effect.sleep("100 millis"); - - return { - timestamp: new Date(), - jobName, - result: `Report generated at ${new Date().toISOString()}`, - }; - }); - -// Schedule with cron expression -const scheduleWithCron = (config: ReportConfig) => - Effect.gen(function* () { - const parsed = parseCronExpression(config.cronExpression); - - yield* Console.log( - `[SCHEDULER] Scheduling job: ${config.jobName}` - ); - yield* Console.log(`[SCHEDULER] Cron: ${config.cronExpression}`); - yield* Console.log(`[SCHEDULER] Timezone: ${config.timezone || "UTC"}\n`); - - // Create schedule that checks every minute - const schedule = Schedule.fixed("1 minute").pipe( - Schedule.untilInputEffect((report: ScheduledReport) => - Effect.gen(function* () { - const isPastTime = shouldRunNow(parsed); - - if (isPastTime) { - yield* Console.log( - `[SCHEDULED] ✓ Running at ${report.timestamp.toISOString()}` - ); - return true; // Stop scheduling - } - - return false; // Continue scheduling - }) - ) - ); - - // Generate report with cron schedule - yield* generateReport(config.jobName).pipe( - Effect.repeat(schedule) - ); - }); - -// Demonstrate multiple cron schedules -const program = Effect.gen(function* () { - console.log( - `\n[START] Scheduling multiple jobs with cron expressions\n` - ); - - // Schedule examples (note: in real app, these would run at actual times) - const jobs = [ - { - cronExpression: "0 9 * * 1-5", // 9 AM weekdays - jobName: "Daily Standup Report", - timezone: "America/New_York", - }, - { - cronExpression: "0 0 * * *", // Midnight daily - jobName: "Nightly Backup", - timezone: "UTC", - }, - { - cronExpression: "0 0 1 * *", // Midnight on 1st of month - jobName: "Monthly Summary", - timezone: "Europe/London", - }, - ]; - - yield* Console.log("[JOBS] Scheduled:"); - jobs.forEach((job) => { - console.log( - ` - ${job.jobName}: ${job.cronExpression} (${job.timezone})` - ); - }); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Fixed intervals don't align with business needs: - -**Fixed interval** (every 24 hours): -- If task takes 2 hours, next run is 26 hours later -- Drifts over time -- No alignment with calendar -- Fails during daylight saving time changes - -**Cron expressions**: -- Specific calendar times (e.g., always 9 AM) -- Independent of execution duration -- Aligns with business hours -- Natural DST handling (clock adjusts, cron resyncs) -- Human-readable vs. milliseconds - -Real-world example: Daily report at 9 AM -- **Fixed interval**: Scheduled at 9:00, takes 1 hour → next at 10:00 → drift until 5 PM -- **Cron `0 9 * * *`**: Always runs at 9:00 regardless of duration or previous delays - ---- - ---- - -## Scheduling Pattern 4: Debounce and Throttle Execution - -**Rule:** Use debounce to wait for silence before executing, and throttle to limit execution frequency, both critical for handling rapid events. - -**Skill Level:** intermediate - -**Use Cases:** scheduling-periodic-tasks - -### Good Example - -This example demonstrates debouncing and throttling for common scenarios. - -```typescript -import { Effect, Schedule, Ref } from "effect"; - -interface SearchQuery { - readonly query: string; - readonly timestamp: Date; -} - -// Simulate API search -const performSearch = (query: string): Effect.Effect => - Effect.gen(function* () { - yield* Effect.log(`[API] Searching for: "${query}"`); - - yield* Effect.sleep("100 millis"); // Simulate API delay - - return [ - `Result 1 for ${query}`, - `Result 2 for ${query}`, - `Result 3 for ${query}`, - ]; - }); - -// Main: demonstrate debounce and throttle -const program = Effect.gen(function* () { - console.log(`\n[DEBOUNCE/THROTTLE] Handling rapid events\n`); - - // Example 1: Debounce search input - console.log(`[1] Debounced search (wait for silence):\n`); - - const searchQueries = ["h", "he", "hel", "hell", "hello"]; - - const debouncedSearches = yield* Ref.make[]>([]); - - for (const query of searchQueries) { - yield* Effect.log(`[INPUT] User typed: "${query}"`); - - // In real app, this would be debounced - yield* Effect.sleep("150 millis"); // User typing - } - - // After user stops, execute search - yield* Effect.log(`[DEBOUNCE] User silent for 200ms, executing search`); - - const searchResults = yield* performSearch("hello"); - - yield* Effect.log(`[RESULTS] ${searchResults.length} results found\n`); - - // Example 2: Throttle scroll events - console.log(`[2] Throttled scroll handler (max 10/sec):\n`); - - const scrollEventCount = yield* Ref.make(0); - const updateCount = yield* Ref.make(0); - - // Simulate 100 rapid scroll events - for (let i = 0; i < 100; i++) { - yield* Ref.update(scrollEventCount, (c) => c + 1); - - // In real app, scroll handler would be throttled - if (i % 10 === 0) { - // Simulate throttled update (max 10 per second) - yield* Ref.update(updateCount, (c) => c + 1); - } - } - - const events = yield* Ref.get(scrollEventCount); - const updates = yield* Ref.get(updateCount); - - yield* Effect.log( - `[THROTTLE] ${events} scroll events → ${updates} updates (${(updates / events * 100).toFixed(1)}% update rate)\n` - ); - - // Example 3: Deduplication - console.log(`[3] Deduplicating rapid events:\n`); - - const userClicks = ["click", "click", "click", "dblclick", "click"]; - - const lastClick = yield* Ref.make(null); - const clickCount = yield* Ref.make(0); - - for (const click of userClicks) { - const prev = yield* Ref.get(lastClick); - - if (click !== prev) { - yield* Effect.log(`[CLICK] Processing: ${click}`); - yield* Ref.update(clickCount, (c) => c + 1); - yield* Ref.set(lastClick, click); - } else { - yield* Effect.log(`[CLICK] Duplicate: ${click} (skipped)`); - } - } - - const processed = yield* Ref.get(clickCount); - - yield* Effect.log( - `\n[DEDUPE] ${userClicks.length} clicks → ${processed} processed\n` - ); - - // Example 4: Exponential backoff on repeated errors - console.log(`[4] Throttled retry on errors:\n`); - - let retryCount = 0; - - const operation = Effect.gen(function* () { - retryCount++; - - if (retryCount < 3) { - yield* Effect.fail(new Error("Still failing")); - } - - yield* Effect.log(`[SUCCESS] Succeeded on attempt ${retryCount}`); - - return "done"; - }).pipe( - Effect.retry( - Schedule.exponential("100 millis").pipe( - Schedule.upTo("1 second"), - Schedule.recurs(5) - ) - ) - ); - - yield* operation; -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Rapid events without debounce/throttle cause problems: - -**Debounce example**: Search input -- User types "hello" character by character -- Without debounce: 5 API calls (one per character) -- With debounce: 1 API call after user stops typing - -**Throttle example**: Scroll events -- Scroll fires 100+ times per second -- Without throttle: Updates lag, GC pressure -- With throttle: Update max 60 times per second - -Real-world issues: -- **API overload**: Search queries hammer backend -- **Rendering lag**: Too many DOM updates -- **Resource exhaustion**: Event handlers never catch up - -Debounce/throttle enable: -- **Efficiency**: Fewer operations -- **Responsiveness**: UI stays smooth -- **Resource safety**: Prevent exhaustion -- **Sanity**: Predictable execution - ---- - ---- - -## Scheduling Pattern 5: Advanced Retry Chains and Circuit Breakers - -**Rule:** Use retry chains with circuit breakers to handle complex failure scenarios, detect cascade failures early, and prevent resource exhaustion. - -**Skill Level:** advanced - -**Use Cases:** scheduling-periodic-tasks - -### Good Example - -This example demonstrates circuit breaker and fallback chain patterns. - -```typescript -import { Effect, Schedule, Ref, Data } from "effect"; - -// Error classification -class RetryableError extends Data.TaggedError("RetryableError")<{ - message: string; - code: string; -}> {} - -class NonRetryableError extends Data.TaggedError("NonRetryableError")<{ - message: string; - code: string; -}> {} - -class CircuitBreakerOpenError extends Data.TaggedError("CircuitBreakerOpenError")<{ - message: string; -}> {} - -// Circuit breaker state -interface CircuitBreakerState { - status: "closed" | "open" | "half-open"; - failureCount: number; - lastFailureTime: Date | null; - successCount: number; -} - -// Create circuit breaker -const createCircuitBreaker = (config: { - failureThreshold: number; - resetTimeoutMs: number; - halfOpenRequests: number; -}) => - Effect.gen(function* () { - const state = yield* Ref.make({ - status: "closed", - failureCount: 0, - lastFailureTime: null, - successCount: 0, - }); - - const recordSuccess = Effect.gen(function* () { - yield* Ref.modify(state, (s) => { - if (s.status === "half-open") { - return [ - undefined, - { - ...s, - successCount: s.successCount + 1, - status: s.successCount + 1 >= config.halfOpenRequests - ? "closed" - : "half-open", - failureCount: 0, - }, - ]; - } - return [undefined, s]; - }); - }); - - const recordFailure = Effect.gen(function* () { - yield* Ref.modify(state, (s) => { - const newFailureCount = s.failureCount + 1; - const newStatus = newFailureCount >= config.failureThreshold - ? "open" - : s.status; - - return [ - undefined, - { - ...s, - failureCount: newFailureCount, - lastFailureTime: new Date(), - status: newStatus, - }, - ]; - }); - }); - - const canExecute = Effect.gen(function* () { - const current = yield* Ref.get(state); - - if (current.status === "closed") { - return true; - } - - if (current.status === "open") { - const timeSinceFailure = Date.now() - (current.lastFailureTime?.getTime() ?? 0); - - if (timeSinceFailure > config.resetTimeoutMs) { - yield* Ref.modify(state, (s) => [ - undefined, - { - ...s, - status: "half-open", - failureCount: 0, - successCount: 0, - }, - ]); - return true; - } - - return false; - } - - // half-open: allow limited requests - return true; - }); - - return { recordSuccess, recordFailure, canExecute, state }; - }); - -// Main example -const program = Effect.gen(function* () { - console.log(`\n[ADVANCED RETRY] Circuit breaker and fallback chains\n`); - - // Create circuit breaker - const cb = yield* createCircuitBreaker({ - failureThreshold: 3, - resetTimeoutMs: 1000, - halfOpenRequests: 2, - }); - - // Example 1: Circuit breaker in action - console.log(`[1] Circuit breaker state transitions:\n`); - - let requestCount = 0; - - const callWithCircuitBreaker = (shouldFail: boolean) => - Effect.gen(function* () { - const canExecute = yield* cb.canExecute; - - if (!canExecute) { - yield* Effect.fail( - new CircuitBreakerOpenError({ - message: "Circuit breaker is open", - }) - ); - } - - requestCount++; - - if (shouldFail) { - yield* cb.recordFailure; - yield* Effect.log( - `[REQUEST ${requestCount}] FAILED (Circuit: ${(yield* Ref.get(cb.state)).status})` - ); - yield* Effect.fail( - new RetryableError({ - message: "Service error", - code: "500", - }) - ); - } else { - yield* cb.recordSuccess; - yield* Effect.log( - `[REQUEST ${requestCount}] SUCCESS (Circuit: ${(yield* Ref.get(cb.state)).status})` - ); - return "success"; - } - }); - - // Simulate failures then recovery - const failSequence = [true, true, true, false, false, false]; - - for (const shouldFail of failSequence) { - yield* callWithCircuitBreaker(shouldFail).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - if (error._tag === "CircuitBreakerOpenError") { - yield* Effect.log( - `[REQUEST ${requestCount + 1}] REJECTED (Circuit open)` - ); - } else { - yield* Effect.log( - `[REQUEST ${requestCount + 1}] ERROR caught` - ); - } - }) - ) - ); - - // Add delay between requests - yield* Effect.sleep("100 millis"); - } - - // Example 2: Fallback chain - console.log(`\n[2] Fallback chain (primary → secondary → cache):\n`); - - const endpoints = { - primary: "https://api.primary.com/data", - secondary: "https://api.secondary.com/data", - cache: "cached-data", - }; - - const callEndpoint = (name: string, shouldFail: boolean) => - Effect.gen(function* () { - yield* Effect.log(`[CALL] Trying ${name}`); - - if (shouldFail) { - yield* Effect.sleep("50 millis"); - yield* Effect.fail( - new RetryableError({ - message: `${name} failed`, - code: "500", - }) - ); - } - - yield* Effect.sleep("50 millis"); - return `data-from-${name}`; - }); - - const fallbackChain = callEndpoint("primary", true).pipe( - Effect.orElse(() => callEndpoint("secondary", false)), - Effect.orElse(() => { - yield* Effect.log(`[FALLBACK] Using cached data`); - return Effect.succeed(endpoints.cache); - }) - ); - - const result = yield* fallbackChain; - - yield* Effect.log(`[RESULT] Got: ${result}\n`); - - // Example 3: Error-specific retry strategy - console.log(`[3] Error classification and adaptive retry:\n`); - - const classifyError = (code: string) => { - if (["502", "503", "504"].includes(code)) { - return "retryable-service-error"; - } - if (["408", "429"].includes(code)) { - return "retryable-rate-limit"; - } - if (["404", "401", "403"].includes(code)) { - return "non-retryable"; - } - if (code === "timeout") { - return "retryable-network"; - } - return "unknown"; - }; - - const errorCodes = ["500", "404", "429", "503", "timeout"]; - - for (const code of errorCodes) { - const classification = classifyError(code); - const shouldRetry = !classification.startsWith("non-retryable"); - - yield* Effect.log( - `[ERROR ${code}] → ${classification} (Retry: ${shouldRetry})` - ); - } - - // Example 4: Bulkhead pattern - console.log(`\n[4] Bulkhead isolation (limit concurrency per endpoint):\n`); - - const bulkheads = { - "primary-api": { maxConcurrent: 5, currentCount: 0 }, - "secondary-api": { maxConcurrent: 3, currentCount: 0 }, - }; - - const acquirePermit = (endpoint: string) => - Effect.gen(function* () { - const bulkhead = bulkheads[endpoint as keyof typeof bulkheads]; - - if (!bulkhead) { - return false; - } - - if (bulkhead.currentCount < bulkhead.maxConcurrent) { - bulkhead.currentCount++; - return true; - } - - yield* Effect.log( - `[BULKHEAD] ${endpoint} at capacity (${bulkhead.currentCount}/${bulkhead.maxConcurrent})` - ); - - return false; - }); - - // Simulate requests - for (let i = 0; i < 10; i++) { - const endpoint = i < 6 ? "primary-api" : "secondary-api"; - const acquired = yield* acquirePermit(endpoint); - - if (acquired) { - yield* Effect.log( - `[REQUEST] Acquired permit for ${endpoint}` - ); - } - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Simple retry fails in production: - -**Scenario 1: Cascade Failure** -- Service A calls Service B (down) -- Retries pile up, consuming resources -- A gets overloaded trying to recover B -- System collapses - -**Scenario 2: Mixed Failures** -- 404 (not found) - retrying won't help -- 500 (server error) - retrying might help -- Network timeout - retrying might help -- Same retry strategy for all = inefficient - -**Scenario 3: Thundering Herd** -- 10,000 clients all retrying at once -- Server recovers, gets hammered again -- Needs coordinated backoff + jitter - -Solutions: - -**Circuit breaker**: -- Monitor error rate -- Stop requests when high -- Resume gradually -- Prevent cascade failures - -**Fallback chain**: -- Try primary endpoint -- Try secondary endpoint -- Use cache -- Return degraded result - -**Adaptive retry**: -- Classify error type -- Use appropriate strategy -- Skip unretryable errors -- Adjust backoff dynamically - ---- - ---- - -## Send a JSON Response - -**Rule:** Use Http.response.json to automatically serialize data structures into a JSON response. - -**Skill Level:** beginner - -**Use Cases:** building-apis - -### Good Example - -This example defines a route that fetches a user object and returns it as a JSON response. The `Http.response.json` function handles all the necessary serialization and header configuration. - -```typescript -import { Effect, Context, Duration, Layer } from "effect"; -import { NodeContext, NodeHttpServer } from "@effect/platform-node"; -import { createServer } from "node:http"; - -const PORT = 3459; // Changed port to avoid conflicts - -// Define HTTP Server service -class JsonServer extends Effect.Service()("JsonServer", { - sync: () => ({ - handleRequest: () => - Effect.succeed({ - status: 200, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - message: "Hello, JSON!", - timestamp: new Date().toISOString(), - }), - }), - }), -}) {} - -// Create and run the server -const program = Effect.gen(function* () { - const jsonServer = yield* JsonServer; - - // Create and start HTTP server - const server = createServer((req, res) => { - const requestHandler = Effect.gen(function* () { - try { - const response = yield* jsonServer.handleRequest(); - res.writeHead(response.status, response.headers); - res.end(response.body); - // Log the response for demonstration - yield* Effect.logInfo(`Sent JSON response: ${response.body}`); - } catch (error: any) { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Internal Server Error" })); - yield* Effect.logError(`Request error: ${error.message}`); - } - }); - - Effect.runPromise(requestHandler); - }); - - // Start server with error handling - yield* Effect.async((resume) => { - server.on("error", (error: NodeJS.ErrnoException) => { - if (error.code === "EADDRINUSE") { - resume(Effect.fail(new Error(`Port ${PORT} is already in use`))); - } else { - resume(Effect.fail(error)); - } - }); - - server.listen(PORT, () => { - resume(Effect.succeed(void 0)); - }); - }); - - yield* Effect.logInfo(`Server running at http://localhost:${PORT}`); - yield* Effect.logInfo("Try: curl http://localhost:3459"); - - // Run for a short time to demonstrate - yield* Effect.sleep(Duration.seconds(3)); - - // Shutdown gracefully - yield* Effect.sync(() => server.close()); - yield* Effect.logInfo("Server shutdown complete"); -}).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Server error: ${error.message}`); - return error; - }) - ), - // Merge layers and provide them in a single call to ensure proper lifecycle management - Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer)) -); - -// Run the program -// Use Effect.runFork for server applications that shouldn't resolve the promise -Effect.runPromise( - program.pipe( - // Ensure the Effect has no remaining context requirements for runPromise - Effect.map(() => undefined) - ) -); -``` - -### Anti-Pattern - -The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -const getUserRoute = Http.router.get( - "/users/1", - Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe( - Effect.flatMap((user) => { - // Manually serialize the object to a JSON string. - const jsonString = JSON.stringify(user); - // Create a text response with the string. - const response = Http.response.text(jsonString); - // Manually set the Content-Type header. - return Effect.succeed( - Http.response.setHeader( - response, - "Content-Type", - "application/json; charset=utf-8" - ) - ); - }) - ) -); - -const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute)); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the `setHeader` call, many clients will fail to parse the response correctly. The `Http.response.json` helper eliminates this entire class of potential bugs. - -### Explanation - -APIs predominantly communicate using JSON. The `Http` module provides a dedicated `Http.response.json` helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone. - -Using `Http.response.json` is superior because: - -1. **Automatic Serialization**: It safely handles the `JSON.stringify` operation for you, including handling potential circular references or other serialization errors. -2. **Correct Headers**: It automatically sets the `Content-Type: application/json; charset=utf-8` header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs. -3. **Simplicity and Readability**: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP. -4. **Composability**: It creates a standard `Http.response` object that works seamlessly with all other parts of the Effect `Http` module. - ---- - ---- - -## Sequencing with andThen, tap, and flatten - -**Rule:** Use sequencing combinators to run computations in order, perform side effects, or flatten nested structures, while preserving error and context handling. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Stream, Option, Either } from "effect"; - -// andThen: Run one effect, then another, ignore the first result -const logThenCompute = Effect.log("Starting...").pipe( - Effect.andThen(Effect.succeed(42)) -); // Effect - -// tap: Log the result of an effect, but keep the value -const computeAndLog = Effect.succeed(42).pipe( - Effect.tap((n) => Effect.log(`Result is ${n}`)) -); // Effect - -// flatten: Remove one level of nesting -const nestedOption = Option.some(Option.some(1)); -const flatOption = Option.flatten(nestedOption); // Option - -const nestedEffect = Effect.succeed(Effect.succeed(1)); -const flatEffect = Effect.flatten(nestedEffect); // Effect - -// tapError: Log errors without handling them -const mightFail = Effect.fail("fail!").pipe( - Effect.tapError((err) => Effect.logError(`Error: ${err}`)) -); // Effect - -// Stream: tap for side effects on each element -const stream = Stream.fromIterable([1, 2, 3]).pipe( - Stream.tap((n) => Effect.log(`Saw: ${n}`)) -); // Stream -``` - -**Explanation:** - -- `andThen` is for sequencing when you don’t care about the first result. -- `tap` is for running side effects (like logging) without changing the value. -- `flatten` is for removing unnecessary nesting (e.g., `Option>` → `Option`). - -### Anti-Pattern - -Using `flatMap` with a function that ignores its argument, or manually unwrapping and re-wrapping nested structures, instead of using the dedicated combinators. - -### Explanation - -Sequencing is fundamental for expressing workflows. -These combinators let you: - -- Run computations in order (`andThen`) -- Attach logging, metrics, or other side effects (`tap`) -- Simplify nested structures (`flatten`) - -All while preserving composability, error handling, and type safety. - ---- - -## Set Up a New Effect Project - -**Rule:** Set up a new Effect project. - -**Skill Level:** beginner - -**Use Cases:** project-setup--execution - -### Good Example - -```typescript -// 1. Init project (e.g., `npm init -y`) -// 2. Install deps (e.g., `npm install effect`, `npm install -D typescript tsx`) -// 3. Create tsconfig.json with `"strict": true` -// 4. Create src/index.ts -import { Effect } from "effect"; - -const program = Effect.log("Hello, World!"); - -Effect.runSync(program); - -// 5. Run the program (e.g., `npx tsx src/index.ts`) -``` - -**Explanation:** -This setup ensures you have TypeScript and Effect ready to go, with strict -type-checking for maximum safety and correctness. - -### Anti-Pattern - -Avoid disabling `strict` mode in your `tsconfig.json`. Running with -`"strict": false` will cause you to lose many of the type-safety guarantees -that make Effect so powerful. - -### Explanation - -A proper setup is crucial for leveraging Effect's powerful type-safety -features. Using TypeScript's `strict` mode is non-negotiable. - ---- - -## Set Up Alerting - -**Rule:** Create alerts based on SLOs and symptoms, not causes. - -**Skill Level:** advanced - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, Metric, Schedule, Duration, Ref } from "effect" - -// ============================================ -// 1. Define alertable conditions -// ============================================ - -interface Alert { - readonly name: string - readonly severity: "critical" | "warning" | "info" - readonly message: string - readonly timestamp: Date - readonly labels: Record -} - -interface AlertRule { - readonly name: string - readonly condition: Effect.Effect - readonly severity: "critical" | "warning" | "info" - readonly message: string - readonly labels: Record - readonly forDuration: Duration.DurationInput -} - -// ============================================ -// 2. Define alert rules -// ============================================ - -const createAlertRules = (metrics: { - errorRate: () => Effect.Effect - latencyP99: () => Effect.Effect - availability: () => Effect.Effect -}): AlertRule[] => [ - { - name: "HighErrorRate", - condition: metrics.errorRate().pipe(Effect.map((rate) => rate > 0.01)), - severity: "critical", - message: "Error rate exceeds 1%", - labels: { team: "backend", service: "api" }, - forDuration: "5 minutes", - }, - { - name: "HighLatency", - condition: metrics.latencyP99().pipe(Effect.map((p99) => p99 > 2)), - severity: "warning", - message: "P99 latency exceeds 2 seconds", - labels: { team: "backend", service: "api" }, - forDuration: "10 minutes", - }, - { - name: "LowAvailability", - condition: metrics.availability().pipe(Effect.map((avail) => avail < 99.9)), - severity: "critical", - message: "Availability below 99.9% SLO", - labels: { team: "backend", service: "api" }, - forDuration: "5 minutes", - }, - { - name: "ErrorBudgetLow", - condition: Effect.succeed(false), // Implement based on error budget calc - severity: "warning", - message: "Error budget below 25%", - labels: { team: "backend", service: "api" }, - forDuration: "0 seconds", - }, -] - -// ============================================ -// 3. Alert manager -// ============================================ - -interface AlertState { - readonly firing: Map - readonly resolved: Alert[] -} - -const makeAlertManager = Effect.gen(function* () { - const state = yield* Ref.make({ - firing: new Map(), - resolved: [], - }) - - const checkRule = (rule: AlertRule) => - Effect.gen(function* () { - const isTriggered = yield* rule.condition - - yield* Ref.modify(state, (s) => { - const firing = new Map(s.firing) - const resolved = [...s.resolved] - const key = rule.name - - if (isTriggered) { - if (!firing.has(key)) { - // New alert - firing.set(key, { - since: new Date(), - alert: { - name: rule.name, - severity: rule.severity, - message: rule.message, - timestamp: new Date(), - labels: rule.labels, - }, - }) - } - } else { - if (firing.has(key)) { - // Alert resolved - const prev = firing.get(key)! - resolved.push({ - ...prev.alert, - message: `[RESOLVED] ${prev.alert.message}`, - timestamp: new Date(), - }) - firing.delete(key) - } - } - - return [undefined, { firing, resolved }] - }) - }) - - const getActiveAlerts = () => - Ref.get(state).pipe( - Effect.map((s) => Array.from(s.firing.values()).map((f) => f.alert)) - ) - - const getRecentResolved = () => - Ref.get(state).pipe(Effect.map((s) => s.resolved.slice(-10))) - - return { - checkRule, - getActiveAlerts, - getRecentResolved, - } -}) - -// ============================================ -// 4. Alert notification -// ============================================ - -interface NotificationChannel { - readonly send: (alert: Alert) => Effect.Effect -} - -const slackChannel: NotificationChannel = { - send: (alert) => - Effect.gen(function* () { - const emoji = - alert.severity === "critical" - ? "🔴" - : alert.severity === "warning" - ? "🟡" - : "🔵" - - yield* Effect.log(`${emoji} [${alert.severity.toUpperCase()}] ${alert.name}`).pipe( - Effect.annotateLogs({ - message: alert.message, - labels: JSON.stringify(alert.labels), - }) - ) - - // In real implementation: call Slack API - }), -} - -const pagerDutyChannel: NotificationChannel = { - send: (alert) => - Effect.gen(function* () { - if (alert.severity === "critical") { - yield* Effect.log("PagerDuty: Creating incident").pipe( - Effect.annotateLogs({ alert: alert.name }) - ) - // In real implementation: call PagerDuty API - } - }), -} - -// ============================================ -// 5. Alert evaluation loop -// ============================================ - -const runAlertEvaluation = ( - rules: AlertRule[], - channels: NotificationChannel[], - interval: Duration.DurationInput -) => - Effect.gen(function* () { - const alertManager = yield* makeAlertManager - const previousAlerts = yield* Ref.make(new Set()) - - yield* Effect.forever( - Effect.gen(function* () { - // Check all rules - for (const rule of rules) { - yield* alertManager.checkRule(rule) - } - - // Get current active alerts - const active = yield* alertManager.getActiveAlerts() - const current = new Set(active.map((a) => a.name)) - const previous = yield* Ref.get(previousAlerts) - - // Find newly firing alerts - for (const alert of active) { - if (!previous.has(alert.name)) { - // New alert - send notifications - for (const channel of channels) { - yield* channel.send(alert) - } - } - } - - yield* Ref.set(previousAlerts, current) - yield* Effect.sleep(interval) - }) - ) - }) - -// ============================================ -// 6. Prometheus alerting rules (YAML) -// ============================================ - -const prometheusAlertRules = ` -groups: - - name: effect-app-alerts - rules: - - alert: HighErrorRate - expr: | - sum(rate(http_errors_total[5m])) - / - sum(rate(http_requests_total[5m])) - > 0.01 - for: 5m - labels: - severity: critical - annotations: - summary: "High error rate detected" - description: "Error rate is {{ $value | humanizePercentage }}" - - - alert: HighLatency - expr: | - histogram_quantile(0.99, - sum(rate(http_request_duration_seconds_bucket[5m])) by (le) - ) > 2 - for: 10m - labels: - severity: warning - annotations: - summary: "High P99 latency" - description: "P99 latency is {{ $value }}s" - - - alert: SLOViolation - expr: | - sum(rate(http_requests_total{status!~"5.."}[30m])) - / - sum(rate(http_requests_total[30m])) - < 0.999 - for: 5m - labels: - severity: critical - annotations: - summary: "SLO violation" - description: "Availability is {{ $value | humanizePercentage }}" -` -``` - -### Explanation - -Good alerting: - -1. **Catches real problems** - Alerts when users are affected -2. **Reduces noise** - Fewer false positives -3. **Enables response** - Actionable information -4. **Supports SLOs** - Tracks service level objectives - ---- - ---- - -## Set Up CI/CD for Effect Projects - -**Rule:** Use GitHub Actions with proper caching for fast Effect project CI/CD. - -**Skill Level:** intermediate - -**Use Cases:** tooling-and-debugging - -### Good Example - -### 1. Basic GitHub Actions Workflow - -```yaml -# .github/workflows/ci.yml -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20.x, 22.x] - - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Install dependencies - run: bun install - - - name: Type check - run: bun run typecheck - - - name: Lint - run: bun run lint - - - name: Test - run: bun run test - - - name: Build - run: bun run build -``` - -### 2. With Caching - -```yaml -# .github/workflows/ci-cached.yml -name: CI (Cached) - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - # Cache Bun dependencies - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.bun/install/cache - node_modules - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} - restore-keys: | - ${{ runner.os }}-bun- - - - name: Install dependencies - run: bun install - - # Cache TypeScript build info - - name: Cache TypeScript - uses: actions/cache@v4 - with: - path: | - .tsbuildinfo - dist - key: ${{ runner.os }}-tsc-${{ hashFiles('**/tsconfig.json', 'src/**/*.ts') }} - restore-keys: | - ${{ runner.os }}-tsc- - - - name: Type check - run: bun run typecheck - - - name: Lint - run: bun run lint - - - name: Test - run: bun run test --coverage - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - files: ./coverage/lcov.info -``` - -### 3. Package.json Scripts - -```json -{ - "scripts": { - "typecheck": "tsc --noEmit", - "lint": "biome check .", - "lint:fix": "biome check --apply .", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "build": "tsc", - "clean": "rm -rf dist .tsbuildinfo" - } -} -``` - -### 4. Multi-Stage Workflow - -```yaml -# .github/workflows/ci-full.yml -name: CI Full - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - - run: bun install - - run: bun run lint - - typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - - run: bun install - - run: bun run typecheck - - test: - runs-on: ubuntu-latest - needs: [lint, typecheck] - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - - run: bun install - - run: bun run test - - build: - runs-on: ubuntu-latest - needs: [test] - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - - run: bun install - - run: bun run build - - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - - deploy: - runs-on: ubuntu-latest - needs: [build] - if: github.ref == 'refs/heads/main' - steps: - - uses: actions/download-artifact@v4 - with: - name: dist - # Add deployment steps -``` - -### 5. Release Workflow - -```yaml -# .github/workflows/release.yml -name: Release - -on: - push: - tags: - - 'v*' - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v1 - - - run: bun install - - run: bun run build - - run: bun run test - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - files: dist/* - generate_release_notes: true -``` - -### Explanation - -CI/CD for Effect projects ensures: - -1. **Type safety** - Catch type errors before merge -2. **Test coverage** - Run tests automatically -3. **Consistent builds** - Same environment every time -4. **Fast feedback** - Know quickly if something broke - ---- - ---- - -## Set Up Your Effect Development Environment - -**Rule:** Install the Effect extension and configure TypeScript for optimal Effect development. - -**Skill Level:** beginner - -**Use Cases:** tooling-and-debugging - -### Explanation - -A well-configured environment helps you: - -1. **See types clearly** - Effect types can be complex -2. **Get better autocomplete** - Know what methods are available -3. **Catch errors early** - TypeScript finds problems -4. **Navigate easily** - Go to definitions, find references - ---- - ---- - -## Sink Pattern 1: Batch Insert Stream Records into Database - -**Rule:** Batch stream records before database operations to improve throughput and reduce transaction overhead. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates streaming user records from a paginated API and batching them for efficient database insertion. - -```typescript -import { Effect, Stream, Sink, Chunk } from "effect"; - -interface User { - readonly id: number; - readonly name: string; - readonly email: string; -} - -interface PaginatedResponse { - readonly users: User[]; - readonly nextPage: number | null; -} - -// Mock API that returns paginated users -const fetchUserPage = ( - page: number -): Effect.Effect => - Effect.succeed( - page < 10 - ? { - users: Array.from({ length: 50 }, (_, i) => ({ - id: page * 50 + i, - name: `User ${page * 50 + i}`, - email: `user${page * 50 + i}@example.com`, - })), - nextPage: page + 1, - } - : { users: [], nextPage: null } - ).pipe(Effect.delay("10 millis")); - -// Mock database insert that takes a batch of users -const insertUserBatch = ( - users: readonly User[] -): Effect.Effect => - Effect.sync(() => { - console.log(`Inserting batch of ${users.length} users`); - return users.length; - }).pipe(Effect.delay("50 millis")); - -// Create a stream of users from paginated API -const userStream: Stream.Stream = Stream.paginateEffect( - 0, - (page) => - fetchUserPage(page).pipe( - Effect.map((response) => [ - Chunk.fromIterable(response.users), - response.nextPage !== null ? Option.some(response.nextPage) : Option.none(), - ]) - ) -); - -// Sink that batches users and inserts them -const batchInsertSink: Sink.Sink = Sink.fold( - 0, - (count, chunk: Chunk.Chunk) => - Effect.gen(function* () { - const users = Chunk.toArray(chunk); - const inserted = yield* insertUserBatch(users); - return count + inserted; - }), - (count) => Effect.succeed(count) -).pipe( - // Batch into groups of 100 users - Sink.withChunking((chunk) => - chunk.pipe( - Chunk.chunksOf(100), - Stream.fromIterable, - Stream.runCollect - ) - ) -); - -// Run the stream with batching sink -const program = Effect.gen(function* () { - const totalInserted = yield* userStream.pipe( - Stream.run(batchInsertSink) - ); - console.log(`Total users inserted: ${totalInserted}`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Creates a stream** of users from a paginated API -2. **Defines a batching sink** that collects users into groups of 100 -3. **Inserts each batch** to the database in a single operation -4. **Tracks total count** of inserted records - -The batching happens automatically—the sink collects elements until the batch size is reached, then processes the complete batch. - ---- - -### Explanation - -Inserting records one-by-one is inefficient: - -- Each insert is a separate database call (network latency, connection overhead) -- Each insert may be a separate transaction (ACID overhead) -- Resource contention and connection pool exhaustion at scale - -Batching solves this by: - -- Grouping N records into a single bulk insert operation -- Amortizing database overhead across multiple records -- Maintaining throughput even under backpressure -- Enabling efficient transaction semantics for the entire batch - -For example, inserting 10,000 records one-by-one might take 100 seconds. Batching in groups of 100 might take just 2-3 seconds. - ---- - ---- - -## Sink Pattern 2: Write Stream Events to Event Log - -**Rule:** Append stream events to an event log with metadata to maintain a complete, ordered record of what happened. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates an event sourcing pattern where a user account stream of events is appended to an event log with metadata. - -```typescript -import { Effect, Stream, Sink, DateTime, Data } from "effect"; - -// Event types -type AccountEvent = - | AccountCreated - | MoneyDeposited - | MoneyWithdrawn - | AccountClosed; - -class AccountCreated extends Data.TaggedError("AccountCreated")<{ - readonly accountId: string; - readonly owner: string; - readonly initialBalance: number; -}> {} - -class MoneyDeposited extends Data.TaggedError("MoneyDeposited")<{ - readonly accountId: string; - readonly amount: number; -}> {} - -class MoneyWithdrawn extends Data.TaggedError("MoneyWithdrawn")<{ - readonly accountId: string; - readonly amount: number; -}> {} - -class AccountClosed extends Data.TaggedError("AccountClosed")<{ - readonly accountId: string; -}> {} - -// Event envelope with metadata -interface StoredEvent { - readonly eventId: string; // Unique identifier per event - readonly eventType: string; // Type of event - readonly aggregateId: string; // What this event is about - readonly aggregateType: string; // What kind of thing (Account) - readonly data: any; // Event payload - readonly metadata: { - readonly timestamp: number; - readonly version: number; // Position in log - readonly causationId?: string; // What caused this - }; -} - -// Mock event log that appends events -const eventLog: StoredEvent[] = []; -let eventVersion = 0; - -const appendToEventLog = ( - event: AccountEvent, - aggregateId: string -): Effect.Effect => - Effect.gen(function* () { - const now = yield* DateTime.now; - const storedEvent: StoredEvent = { - eventId: `evt-${eventVersion}-${Date.now()}`, - eventType: event._tag, - aggregateId, - aggregateType: "Account", - data: event, - metadata: { - timestamp: now.toEpochMillis(), - version: ++eventVersion, - }, - }; - - // Append to log (simulated) - eventLog.push(storedEvent); - console.log( - `[v${storedEvent.metadata.version}] ${storedEvent.eventType}: ${aggregateId}` - ); - - return storedEvent; - }); - -// Simulate a stream of events from various account operations -const accountEvents: Stream.Stream<[string, AccountEvent]> = Stream.fromIterable([ - [ - "acc-1", - new AccountCreated({ - accountId: "acc-1", - owner: "Alice", - initialBalance: 1000, - }), - ], - ["acc-1", new MoneyDeposited({ accountId: "acc-1", amount: 500 })], - ["acc-1", new MoneyWithdrawn({ accountId: "acc-1", amount: 200 })], - [ - "acc-2", - new AccountCreated({ - accountId: "acc-2", - owner: "Bob", - initialBalance: 2000, - }), - ], - ["acc-2", new MoneyDeposited({ accountId: "acc-2", amount: 1000 })], - ["acc-1", new AccountClosed({ accountId: "acc-1" })], -]); - -// Sink that appends each event to the log -const eventLogSink: Sink.Sink = Sink.fold( - 0, - (count, [aggregateId, event]) => - appendToEventLog(event, aggregateId).pipe( - Effect.map(() => count + 1) - ), - (count) => Effect.succeed(count) -); - -// Run the stream and append all events -const program = Effect.gen(function* () { - const totalEvents = yield* accountEvents.pipe(Stream.run(eventLogSink)); - - console.log(`\nTotal events appended: ${totalEvents}`); - console.log(`\nEvent log contents:`); - eventLog.forEach((event) => { - console.log(` [v${event.metadata.version}] ${event.eventType}`); - }); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Defines event types** using tagged errors (AccountCreated, MoneyDeposited, etc.) -2. **Creates event envelopes** with metadata (timestamp, version, causation) -3. **Streams events** from various sources -4. **Appends to log** with proper versioning and ordering -5. **Maintains history** for reconstruction and audit - ---- - -### Explanation - -Event logs are foundational to many patterns: - -- **Event Sourcing**: Instead of storing current state, store the sequence of events that led to it -- **Audit Trails**: Complete, tamper-proof record of who did what and when -- **Temporal Queries**: Reconstruct state at any point in time -- **Consistency**: Single source of truth for what happened -- **Replay**: Rebuild state or test changes by replaying events - -Unlike batch inserts which are transactional, event logs are append-only. Each event is immutable once written. This simplicity enables: - -- Fast appends (no updates, just sequential writes) -- Natural ordering (events in write order) -- Easy distribution (replicate the log) -- Strong consistency (events are facts that don't change) - ---- - ---- - -## Sink Pattern 3: Write Stream Lines to File - -**Rule:** Write streaming lines to a file efficiently using buffered output and proper resource management. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates streaming log entries and writing them to a file with buffering. - -```typescript -import { Effect, Stream, Sink, Chunk, FileSystem } from "effect"; - -interface LogEntry { - readonly level: "debug" | "info" | "warn" | "error"; - readonly message: string; - readonly timestamp: number; -} - -// Format a log entry as a line -const formatLogLine = (entry: LogEntry): string => { - const iso = new Date(entry.timestamp).toISOString(); - return `[${iso}] ${entry.level.toUpperCase()}: ${entry.message}`; -}; - -// Simulate a stream of log entries -const logStream: Stream.Stream = Stream.fromIterable([ - { level: "info", message: "Server starting", timestamp: Date.now() }, - { level: "debug", message: "Loading config", timestamp: Date.now() + 100 }, - { level: "info", message: "Connected to database", timestamp: Date.now() + 200 }, - { level: "warn", message: "High memory usage detected", timestamp: Date.now() + 300 }, - { level: "info", message: "Processing request", timestamp: Date.now() + 400 }, - { level: "error", message: "Connection timeout", timestamp: Date.now() + 500 }, - { level: "info", message: "Retrying connection", timestamp: Date.now() + 600 }, - { level: "info", message: "Connection restored", timestamp: Date.now() + 700 }, -]); - -// Create a file writer sink with buffering -const createFileWriteSink = ( - filePath: string, - bufferSize: number = 100 -): Sink.Sink => - Effect.scoped( - Effect.gen(function* () { - // Open file in append mode - const fs = yield* FileSystem.FileSystem; - const handle = yield* fs.open(filePath, "a"); - - let buffer: string[] = []; - let lineCount = 0; - - // Flush buffered lines to disk - const flush = Effect.gen(function* () { - if (buffer.length === 0) return; - - const content = buffer.join("\n") + "\n"; - yield* fs.write(handle, content); - buffer = []; - }); - - // Return the sink - return Sink.fold( - 0, - (count, line: string) => - Effect.gen(function* () { - buffer.push(line); - const newCount = count + 1; - - // Flush when buffer reaches size limit - if (buffer.length >= bufferSize) { - yield* flush; - } - - return newCount; - }), - (count) => - Effect.gen(function* () { - // Flush any remaining lines before closing - yield* flush; - yield* fs.close(handle); - return count; - }) - ); - }) - ).pipe( - Effect.flatten - ); - -// Process the log stream -const program = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const filePath = "/tmp/app.log"; - - // Clear the file first - yield* fs.writeFileString(filePath, ""); - - // Stream logs, format them, and write to file - const written = yield* logStream.pipe( - Stream.map(formatLogLine), - Stream.run(createFileWriteSink(filePath, 50)) // Buffer 50 lines before flush - ); - - console.log(`Wrote ${written} log lines to ${filePath}`); - - // Read back the file to verify - const content = yield* fs.readFileString(filePath); - console.log("\nFile contents:"); - console.log(content); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Opens a file** for appending -2. **Buffers log lines** in memory (50 lines before flush) -3. **Flushes periodically** when buffer fills or stream ends -4. **Closes the file** safely using scopes -5. **Tracks line count** for confirmation - ---- - -### Explanation - -Writing stream data to files requires: - -- **Buffering**: Writing one line at a time is slow. Buffer multiple lines before flushing to disk -- **Efficiency**: Reduce system calls and I/O overhead by batching writes -- **Resource Management**: Ensure file handles are properly closed even on errors -- **Ordering**: Maintain the order of lines as they appear in the stream - -This pattern is essential for: - -- Log files and audit trails -- CSV/JSON Line export -- Streaming data archival -- Data pipelines with file intermediates - ---- - ---- - -## Sink Pattern 4: Send Stream Records to Message Queue - -**Rule:** Stream records to message queues with proper batching and acknowledgment for reliable distributed data flow. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates streaming sensor readings and publishing them to a message queue with topic-based partitioning. - -```typescript -import { Effect, Stream, Sink, Chunk } from "effect"; - -interface SensorReading { - readonly sensorId: string; - readonly location: string; - readonly temperature: number; - readonly humidity: number; - readonly timestamp: number; -} - -// Mock message queue publisher -interface QueuePublisher { - readonly publish: ( - topic: string, - partition: string, - messages: readonly SensorReading[] - ) => Effect.Effect<{ acknowledged: number; messageIds: string[] }>; -} - -// Create a mock queue publisher -const createMockPublisher = (): QueuePublisher => { - const publishedMessages: Record = {}; - - return { - publish: (topic, partition, messages) => - Effect.gen(function* () { - const key = `${topic}/${partition}`; - publishedMessages[key] = [ - ...(publishedMessages[key] ?? []), - ...messages, - ]; - - const messageIds = Array.from({ length: messages.length }, (_, i) => - `msg-${Date.now()}-${i}` - ); - - console.log( - `Published ${messages.length} messages to ${key} (batch)` - ); - - return { acknowledged: messages.length, messageIds }; - }), - }; -}; - -// Determine the partition key based on sensor location -const getPartitionKey = (reading: SensorReading): string => - reading.location; // Route by location for data locality - -// Simulate a stream of sensor readings -const sensorStream: Stream.Stream = Stream.fromIterable([ - { - sensorId: "temp-1", - location: "warehouse-a", - temperature: 22.5, - humidity: 45, - timestamp: Date.now(), - }, - { - sensorId: "temp-2", - location: "warehouse-b", - temperature: 21.0, - humidity: 50, - timestamp: Date.now() + 100, - }, - { - sensorId: "temp-3", - location: "warehouse-a", - temperature: 22.8, - humidity: 46, - timestamp: Date.now() + 200, - }, - { - sensorId: "temp-4", - location: "warehouse-c", - temperature: 20.5, - humidity: 55, - timestamp: Date.now() + 300, - }, - { - sensorId: "temp-5", - location: "warehouse-b", - temperature: 21.2, - humidity: 51, - timestamp: Date.now() + 400, - }, - { - sensorId: "temp-6", - location: "warehouse-a", - temperature: 23.0, - humidity: 47, - timestamp: Date.now() + 500, - }, -]); - -// Create a sink that batches and publishes to message queue -const createQueuePublishSink = ( - publisher: QueuePublisher, - topic: string, - batchSize: number = 100 -): Sink.Sink => - Sink.fold( - { batches: new Map(), totalPublished: 0 }, - (state, reading) => - Effect.gen(function* () { - const partition = getPartitionKey(reading); - const batch = state.batches.get(partition) ?? []; - const newBatch = [...batch, reading]; - - if (newBatch.length >= batchSize) { - // Batch is full, publish it - const result = yield* publisher.publish(topic, partition, newBatch); - const newState = new Map(state.batches); - newState.delete(partition); - - return { - ...state, - batches: newState, - totalPublished: state.totalPublished + result.acknowledged, - }; - } else { - // Add to batch and continue - const newState = new Map(state.batches); - newState.set(partition, newBatch); - - return { ...state, batches: newState }; - } - }), - (state) => - Effect.gen(function* () { - let finalCount = state.totalPublished; - - // Publish any remaining partial batches - for (const [partition, batch] of state.batches) { - if (batch.length > 0) { - const result = yield* publisher.publish(topic, partition, batch); - finalCount += result.acknowledged; - } - } - - return finalCount; - }) - ); - -// Run the stream and publish to queue -const program = Effect.gen(function* () { - const publisher = createMockPublisher(); - const topic = "sensor-readings"; - - const published = yield* sensorStream.pipe( - Stream.run(createQueuePublishSink(publisher, topic, 50)) // Batch size of 50 - ); - - console.log( - `\nTotal messages published to queue: ${published}` - ); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Groups readings by partition** (location) for data locality -2. **Batches records** before publishing (50 at a time) -3. **Publishes batches** to the queue with partition key -4. **Flushes partial batches** when stream ends -5. **Tracks acknowledgments** from the queue - ---- - -### Explanation - -Message queues are the backbone of event-driven architectures: - -- **Decoupling**: Producers don't wait for consumers -- **Scalability**: Multiple subscribers can consume independently -- **Durability**: Messages persist even if subscribers are down -- **Ordering**: Maintain event sequence (per partition/topic) -- **Reliability**: Acknowledgments and retries ensure no message loss - -Unlike direct writes which block, queue publishing is asynchronous and enables: - -- High-throughput publishing (batch multiple records per operation) -- Backpressure handling (queue manages flow) -- Multi-subscriber patterns (fan-out) -- Dead letter queues for error handling - ---- - ---- - -## Sink Pattern 5: Fall Back to Alternative Sink on Failure - -**Rule:** Implement fallback sinks to handle failures gracefully and ensure data is persisted even when the primary destination is unavailable. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates a system that tries to write order records to a fast in-memory cache first, falls back to database if cache fails, and falls back to a dead letter file if database fails. - -```typescript -import { Effect, Stream, Sink, Chunk, Either, Data } from "effect"; - -interface Order { - readonly orderId: string; - readonly customerId: string; - readonly total: number; - readonly timestamp: number; -} - -class CacheSinkError extends Data.TaggedError("CacheSinkError")<{ - readonly reason: string; -}> {} - -class DatabaseSinkError extends Data.TaggedError("DatabaseSinkError")<{ - readonly reason: string; -}> {} - -// Mock in-memory cache sink (fast but limited) -const createCacheSink = (): Sink.Sink => { - const cache: Order[] = []; - const MAX_CACHE_SIZE = 1000; - - return Sink.fold( - 0, - (count, order) => - Effect.gen(function* () { - if (cache.length >= MAX_CACHE_SIZE) { - yield* Effect.fail( - new CacheSinkError({ - reason: `Cache full (${cache.length}/${MAX_CACHE_SIZE})`, - }) - ); - } - - cache.push(order); - console.log(`[CACHE] Cached order ${order.orderId}`); - return count + 1; - }), - (count) => - Effect.gen(function* () { - console.log(`[CACHE] Final: ${count} orders in cache`); - return count; - }) - ); -}; - -// Mock database sink (slower but reliable) -const createDatabaseSink = (): Sink.Sink => { - const orders: Order[] = []; - - return Sink.fold( - 0, - (count, order) => - Effect.gen(function* () { - // Simulate occasional database failures - if (Math.random() < 0.1) { - yield* Effect.fail( - new DatabaseSinkError({ - reason: "Connection timeout", - }) - ); - } - - orders.push(order); - console.log(`[DATABASE] Persisted order ${order.orderId}`); - return count + 1; - }), - (count) => - Effect.gen(function* () { - console.log(`[DATABASE] Final: ${count} orders in database`); - return count; - }) - ); -}; - -// Mock file sink (always works but slow) -const createDeadLetterSink = (): Sink.Sink => { - const deadLetters: Order[] = []; - - return Sink.fold( - 0, - (count, order) => - Effect.gen(function* () { - deadLetters.push(order); - console.log( - `[DEAD-LETTER] Wrote order ${order.orderId} to dead letter file` - ); - return count + 1; - }), - (count) => - Effect.gen(function* () { - console.log( - `[DEAD-LETTER] Final: ${count} orders in dead letter file` - ); - return count; - }) - ); -}; - -// Create a fallback sink that tries cache -> database -> file -const createFallbackSink = (): Sink.Sink< - { readonly cached: number; readonly persisted: number; readonly deadLetters: number }, - never, - Order -> => - Sink.fold( - { cached: 0, persisted: 0, deadLetters: 0 }, - (state, order) => - Effect.gen(function* () { - // Try cache first - const cacheResult = yield* createCacheSink() - .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); - - if (Either.isRight(cacheResult)) { - return { - ...state, - cached: state.cached + cacheResult.right, - }; - } - - console.log( - `[FALLBACK] Cache failed (${cacheResult.left.reason}), trying database` - ); - - // Cache failed, try database - const dbResult = yield* createDatabaseSink() - .pipe(Sink.feed(Chunk.of(order))) - .pipe(Effect.either); - - if (Either.isRight(dbResult)) { - return { - ...state, - persisted: state.persisted + dbResult.right, - }; - } - - console.log( - `[FALLBACK] Database failed (${dbResult.left.reason}), falling back to dead letter` - ); - - // Database failed, use dead letter - const dlResult = yield* createDeadLetterSink() - .pipe(Sink.feed(Chunk.of(order))); - - return { - ...state, - deadLetters: state.deadLetters + dlResult, - }; - }), - (state) => - Effect.gen(function* () { - console.log(`\n[SUMMARY]`); - console.log(` Cached: ${state.cached}`); - console.log(` Persisted: ${state.persisted}`); - console.log(` Dead Letter: ${state.deadLetters}`); - return state; - }) - ); - -// Simulate a stream of orders -const orderStream: Stream.Stream = Stream.fromIterable([ - { - orderId: "order-1", - customerId: "cust-1", - total: 99.99, - timestamp: Date.now(), - }, - { - orderId: "order-2", - customerId: "cust-2", - total: 149.99, - timestamp: Date.now() + 100, - }, - { - orderId: "order-3", - customerId: "cust-1", - total: 49.99, - timestamp: Date.now() + 200, - }, - { - orderId: "order-4", - customerId: "cust-3", - total: 199.99, - timestamp: Date.now() + 300, - }, - { - orderId: "order-5", - customerId: "cust-2", - total: 89.99, - timestamp: Date.now() + 400, - }, -]); - -// Run the stream with fallback sink -const program = Effect.gen(function* () { - const result = yield* orderStream.pipe(Stream.run(createFallbackSink())); - console.log(`\nTotal orders processed: ${result.cached + result.persisted + result.deadLetters}`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Tries cache first** (fast, limited capacity) -2. **Falls back to database** if cache is full -3. **Falls back to dead letter** if database fails -4. **Tracks which sink** was used for each record -5. **Reports summary** of where data went - ---- - -### Explanation - -Production systems need resilience: - -- **Primary failures**: Database down, network timeout, quota exceeded -- **Progressive degradation**: Keep the system running, even at reduced capacity -- **No data loss**: Fallback ensures data is persisted somewhere -- **Operational flexibility**: Choose fallback based on failure type -- **Monitoring**: Track when fallbacks are used to alert operators - -Without fallback patterns: - -- System fails when primary destination fails -- Data is lost if primary is unavailable -- No clear signal that degradation occurred - -With fallback sinks: - -- Stream continues even when primary fails -- Data is safely persisted to alternative -- Clear audit trail of which sink was used - ---- - ---- - -## Sink Pattern 6: Retry Failed Stream Operations - -**Rule:** Implement retry strategies in sinks to handle transient failures and improve resilience without manual intervention. - -**Skill Level:** intermediate - -**Use Cases:** streams-sinks - -### Good Example - -This example demonstrates retrying database writes with exponential backoff, tracking attempts, and falling back on permanent failures. - -```typescript -import { Effect, Stream, Sink, Chunk, Duration, Schedule } from "effect"; - -interface UserRecord { - readonly userId: string; - readonly name: string; - readonly email: string; -} - -class WriteError extends Error { - readonly isTransient: boolean; - - constructor(message: string, isTransient: boolean = true) { - super(message); - this.name = "WriteError"; - this.isTransient = isTransient; - } -} - -// Mock database that occasionally fails -const database = { - failureRate: 0.3, // 30% transient failure rate - permanentFailureRate: 0.05, // 5% permanent failure rate - - insertUser: (user: UserRecord): Effect.Effect => - Effect.gen(function* () { - const rand = Math.random(); - - // Permanent failure (e.g., constraint violation) - if (rand < database.permanentFailureRate) { - throw new WriteError( - `Permanent: User ${user.userId} already exists`, - false - ); - } - - // Transient failure (e.g., connection timeout) - if (rand < database.permanentFailureRate + database.failureRate) { - throw new WriteError( - `Transient: Connection timeout writing ${user.userId}`, - true - ); - } - - // Success - console.log(`✓ Wrote user ${user.userId}`); - }), -}; - -// Retry configuration -interface RetryConfig { - readonly maxAttempts: number; - readonly initialDelayMs: number; - readonly maxDelayMs: number; - readonly backoffFactor: number; -} - -const defaultRetryConfig: RetryConfig = { - maxAttempts: 5, - initialDelayMs: 100, // Start with 100ms - maxDelayMs: 5000, // Cap at 5 seconds - backoffFactor: 2, // Double each time -}; - -// Result tracking -interface OperationResult { - readonly succeeded: number; - readonly transientFailures: number; - readonly permanentFailures: number; - readonly detailedStats: Array<{ - readonly userId: string; - readonly attempts: number; - readonly status: "success" | "transient-failed" | "permanent-failed"; - }>; -} - -// Create a sink with retry logic -const createRetrySink = (config: RetryConfig): Sink.Sink => - Sink.fold( - { - succeeded: 0, - transientFailures: 0, - permanentFailures: 0, - detailedStats: [], - }, - (state, user) => - Effect.gen(function* () { - let lastError: WriteError | null = null; - let attempts = 0; - - // Retry loop - for (attempts = 1; attempts <= config.maxAttempts; attempts++) { - try { - yield* database.insertUser(user); - - // Success! - console.log( - `[${user.userId}] Success on attempt ${attempts}/${config.maxAttempts}` - ); - - return { - ...state, - succeeded: state.succeeded + 1, - detailedStats: [ - ...state.detailedStats, - { - userId: user.userId, - attempts, - status: "success", - }, - ], - }; - } catch (error) { - lastError = error as WriteError; - - if (!lastError.isTransient) { - // Permanent failure, don't retry - console.log( - `[${user.userId}] Permanent failure: ${lastError.message}` - ); - - return { - ...state, - permanentFailures: state.permanentFailures + 1, - detailedStats: [ - ...state.detailedStats, - { - userId: user.userId, - attempts, - status: "permanent-failed", - }, - ], - }; - } - - // Transient failure, retry if attempts remain - if (attempts < config.maxAttempts) { - // Calculate delay with exponential backoff - let delayMs = config.initialDelayMs * Math.pow(config.backoffFactor, attempts - 1); - delayMs = Math.min(delayMs, config.maxDelayMs); - - // Add jitter (±10%) - const jitter = delayMs * 0.1; - delayMs = delayMs + (Math.random() - 0.5) * 2 * jitter; - - console.log( - `[${user.userId}] Transient failure (attempt ${attempts}/${config.maxAttempts}): ${lastError.message}` - ); - console.log(` Retrying in ${Math.round(delayMs)}ms...`); - - yield* Effect.sleep(Duration.millis(Math.round(delayMs))); - } - } - } - - // All retries exhausted - console.log( - `[${user.userId}] Failed after ${config.maxAttempts} attempts` - ); - - return { - ...state, - transientFailures: state.transientFailures + 1, - detailedStats: [ - ...state.detailedStats, - { - userId: user.userId, - attempts: config.maxAttempts, - status: "transient-failed", - }, - ], - }; - }), - (state) => - Effect.gen(function* () { - console.log(`\n[SUMMARY]`); - console.log(` Succeeded: ${state.succeeded}`); - console.log(` Transient Failures: ${state.transientFailures}`); - console.log(` Permanent Failures: ${state.permanentFailures}`); - console.log(` Total: ${state.detailedStats.length}`); - - // Show detailed stats - const failed = state.detailedStats.filter((s) => s.status !== "success"); - if (failed.length > 0) { - console.log(`\n[FAILURES]`); - failed.forEach((stat) => { - console.log( - ` ${stat.userId}: ${stat.attempts} attempts (${stat.status})` - ); - }); - } - - return state; - }) - ); - -// Simulate a stream of users to insert -const userStream: Stream.Stream = Stream.fromIterable([ - { userId: "user-1", name: "Alice", email: "alice@example.com" }, - { userId: "user-2", name: "Bob", email: "bob@example.com" }, - { userId: "user-3", name: "Charlie", email: "charlie@example.com" }, - { userId: "user-4", name: "Diana", email: "diana@example.com" }, - { userId: "user-5", name: "Eve", email: "eve@example.com" }, -]); - -// Run the stream with retry sink -const program = Effect.gen(function* () { - const result = yield* userStream.pipe(Stream.run(createRetrySink(defaultRetryConfig))); - console.log(`\nProcessing complete.`); -}); - -Effect.runPromise(program); -``` - -This pattern: - -1. **Attempts operation** up to max retries -2. **Distinguishes transient vs. permanent failures** -3. **Uses exponential backoff** to space retries -4. **Adds jitter** to prevent thundering herd -5. **Tracks detailed stats** for monitoring -6. **Reports summary** of outcomes - ---- - -### Explanation - -Transient failures are common in distributed systems: - -- **Network timeouts**: Temporary connectivity issues resolve themselves -- **Rate limiting**: Service recovers once rate limit window resets -- **Temporary unavailability**: Services restart or scale up -- **Circuit breaker trips**: Service recovers after backoff period - -Without retry logic: - -- Every transient failure causes data loss or stream interruption -- Manual intervention required to restart -- System appears less reliable than it actually is - -With intelligent retry logic: - -- Automatic recovery from transient failures -- Exponential backoff prevents thundering herd -- Clear visibility into which operations failed permanently -- Data flows continuously despite temporary issues - ---- - ---- - -## Solve Promise Problems with Effect - -**Rule:** Recognize that Effect solves the core limitations of Promises: untyped errors, no dependency injection, and no cancellation. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -This code is type-safe, testable, and cancellable. The signature `Effect.Effect` tells us everything we need to know. - -```typescript -import { Effect, Data } from "effect"; - -interface DbErrorType { - readonly _tag: "DbError"; - readonly message: string; -} - -const DbError = Data.tagged("DbError"); - -interface User { - name: string; -} - -class HttpClient extends Effect.Service()("HttpClient", { - sync: () => ({ - findById: (id: number): Effect.Effect => - Effect.try({ - try: () => ({ name: `User ${id}` }), - catch: () => DbError({ message: "Failed to find user" }), - }), - }), -}) {} - -const findUser = (id: number) => - Effect.gen(function* () { - const client = yield* HttpClient; - return yield* client.findById(id); - }); - -// Demonstrate how Effect solves promise problems -const program = Effect.gen(function* () { - yield* Effect.logInfo("=== Solving Promise Problems with Effect ==="); - - // Problem 1: Proper error handling (no more try/catch hell) - yield* Effect.logInfo("1. Demonstrating type-safe error handling:"); - - const result1 = yield* findUser(123).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logInfo(`Handled error: ${error.message}`); - return { name: "Default User" }; - }) - ) - ); - yield* Effect.logInfo(`Found user: ${result1.name}`); - - // Problem 2: Easy composition and chaining - yield* Effect.logInfo("\n2. Demonstrating easy composition:"); - - const composedOperation = Effect.gen(function* () { - const user1 = yield* findUser(1); - const user2 = yield* findUser(2); - yield* Effect.logInfo(`Composed result: ${user1.name} and ${user2.name}`); - return [user1, user2]; - }); - - yield* composedOperation; - - // Problem 3: Resource management and cleanup - yield* Effect.logInfo("\n3. Demonstrating resource management:"); - - const resourceOperation = Effect.gen(function* () { - yield* Effect.logInfo("Acquiring resource..."); - const resource = "database-connection"; - - yield* Effect.addFinalizer(() => Effect.logInfo("Cleaning up resource...")); - - const user = yield* findUser(456); - yield* Effect.logInfo(`Used resource to get: ${user.name}`); - - return user; - }).pipe(Effect.scoped); - - yield* resourceOperation; - - yield* Effect.logInfo("\n✅ All operations completed successfully!"); -}); - -Effect.runPromise(Effect.provide(program, HttpClient.Default)); -``` - ---- - -### Anti-Pattern - -This `Promise`-based function has several hidden problems that Effect solves: - -- What happens if `db.findUser` rejects? The error is untyped (`any`). -- Where does `db` come from? It's a hidden dependency, making this function hard to test. -- If the operation is slow, how do we cancel it? We can't. - -```typescript -// ❌ This function has hidden dependencies and untyped errors. -async function findUserUnsafely(id: number): Promise { - try { - const user = await db.findUser(id); // `db` is a hidden global or import - return user; - } catch (error) { - // `error` is of type `any`. We don't know what it is. - // We might log it and re-throw, but we can't handle it safely. - throw error; - } -} -``` - -### Explanation - -While `async/await` is great for simple cases, building large, robust applications with `Promise`s reveals these critical gaps. Effect addresses each one directly: - -- **Typed Errors:** The `E` channel in `Effect` forces you to handle specific, known error types, eliminating an entire class of runtime bugs. -- **Dependency Injection:** The `R` channel provides a powerful, built-in system for declaring and providing dependencies (`Layer`s), making your code modular and testable. -- **Cancellation (Interruption):** Effect's structured concurrency and `Fiber` model provide robust, built-in cancellation. When an effect is interrupted, Effect guarantees that its cleanup logic (finalizers) will be run. - -Understanding that Effect was built specifically to solve these problems is key to appreciating its design and power. - ---- - ---- - -## State Management Pattern 1: Synchronized Reference with SynchronizedRef - -**Rule:** Use SynchronizedRef for thread-safe mutable state that must be updated consistently across concurrent operations, with atomic modifications. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates synchronized reference patterns. - -```typescript -import { Effect, Ref, Fiber, Deferred } from "effect"; - -interface Counter { - readonly value: number; - readonly updates: number; -} - -interface Account { - readonly balance: number; - readonly transactions: string[]; -} - -const program = Effect.gen(function* () { - console.log( - `\n[SYNCHRONIZED REFERENCES] Concurrent state management\n` - ); - - // Example 1: Basic counter with atomic updates - console.log(`[1] Atomic counter increments:\n`); - - const counter = yield* Ref.make({ - value: 0, - updates: 0, - }); - - // Simulate 5 concurrent increments - const incrementTasks = Array.from({ length: 5 }, (_, i) => - Effect.gen(function* () { - for (let j = 0; j < 20; j++) { - yield* Ref.modify(counter, (current) => [ - undefined, - { - value: current.value + 1, - updates: current.updates + 1, - }, - ]); - - if (j === 0 || j === 19) { - yield* Effect.log( - `[FIBER ${i}] Increment ${j === 0 ? "start" : "end"}` - ); - } - } - }) - ); - - // Run concurrently - yield* Effect.all(incrementTasks, { concurrency: "unbounded" }); - - const finalCounter = yield* Ref.get(counter); - - yield* Effect.log( - `[RESULT] Counter: ${finalCounter.value} (expected 100)` - ); - yield* Effect.log( - `[RESULT] Updates: ${finalCounter.updates} (expected 100)\n` - ); - - // Example 2: Bank account with transaction isolation - console.log(`[2] Account with atomic transfers:\n`); - - const account = yield* Ref.make({ - balance: 1000, - transactions: [], - }); - - const transfer = (amount: number, description: string) => - Ref.modify(account, (current) => { - if (current.balance < amount) { - // Insufficient funds, don't modify - return [ - { success: false, reason: "insufficient-funds" }, - current, // Unchanged - ]; - } - - // Atomic: deduct + record transaction - return [ - { success: true, reason: "transferred" }, - { - balance: current.balance - amount, - transactions: [ - ...current.transactions, - `${description}: -$${amount}`, - ], - }, - ]; - }); - - // Test transfer - const t1 = yield* transfer(100, "Coffee"); - - yield* Effect.log(`[TRANSFER 1] ${t1.success ? "✓" : "✗"} ${t1.reason}`); - - const t2 = yield* transfer(2000, "Electronics"); - - yield* Effect.log(`[TRANSFER 2] ${t2.success ? "✓" : "✗"} ${t2.reason}`); - - const t3 = yield* transfer(200, "Groceries"); - - yield* Effect.log(`[TRANSFER 3] ${t3.success ? "✓" : "✗"} ${t3.reason}\n`); - - // Example 3: Concurrent reads don't block writes - console.log(`[3] Concurrent reads and writes:\n`); - - const state = yield* Ref.make({ value: 0, readers: 0 }); - - const read = Effect.gen(function* () { - const snapshot = yield* Ref.get(state); - - yield* Effect.log( - `[READ] Got value: ${snapshot.value}` - ); - - return snapshot.value; - }); - - const write = (newValue: number) => - Ref.set(state, { value: newValue, readers: 0 }); - - // Concurrent operations - const mixed = Effect.all( - [ - read, - write(10), - read, - write(20), - read, - ], - { concurrency: "unbounded" } - ); - - yield* mixed; - - // Example 4: Compare-and-set pattern (retry on failure) - console.log(`\n[4] Compare-and-set (optimistic updates):\n`); - - const versionedState = yield* Ref.make({ version: 0, data: "initial" }); - - const updateWithVersion = (newData: string) => - Effect.gen(function* () { - let retries = 0; - - while (retries < 3) { - const current = yield* Ref.get(versionedState); - - // Try to update (check-and-set) - const result = yield* Ref.modify(versionedState, (s) => { - if (s.version === current.version) { - // No concurrent update, proceed - return [ - { success: true }, - { - version: s.version + 1, - data: newData, - }, - ]; - } - - // Version changed, conflict - return [{ success: false }, s]; - }); - - if (result.success) { - yield* Effect.log( - `[CAS] Updated on attempt ${retries + 1}` - ); - - return true; - } - - retries++; - - yield* Effect.log( - `[CAS] Conflict detected, retrying (attempt ${retries + 1})` - ); - } - - return false; - }); - - const casResult = yield* updateWithVersion("updated-data"); - - yield* Effect.log(`[CAS] Success: ${casResult}\n`); - - // Example 5: State with subscriptions (notify on change) - console.log(`[5] State changes with notification:\n`); - - interface Notification { - oldValue: unknown; - newValue: unknown; - timestamp: Date; - } - - const observedState = yield* Ref.make<{ value: number; lastChange: Date }>({ - value: 0, - lastChange: new Date(), - }); - - const updateAndNotify = (newValue: number) => - Ref.modify(observedState, (current) => { - const notification: Notification = { - oldValue: current.value, - newValue, - timestamp: new Date(), - }; - - yield* Effect.log( - `[NOTIFY] ${current.value} → ${newValue} at ${notification.timestamp.toISOString()}` - ); - - return [ - notification, - { - value: newValue, - lastChange: notification.timestamp, - }, - ]; - }); - - // Trigger changes - for (const val of [5, 10, 15]) { - yield* updateAndNotify(val); - } - - // Example 6: Atomic batch updates - console.log(`\n[6] Batch atomic updates:\n`); - - interface BatchState { - items: string[]; - locked: boolean; - version: number; - } - - const batchState = yield* Ref.make({ - items: [], - locked: false, - version: 0, - }); - - const addItems = (newItems: string[]) => - Ref.modify(batchState, (current) => { - // All items added atomically - return [ - { added: newItems.length }, - { - items: [...current.items, ...newItems], - locked: false, - version: current.version + 1, - }, - ]; - }); - - const batch1 = yield* addItems(["item1", "item2", "item3"]); - - yield* Effect.log( - `[BATCH 1] Added ${batch1.added} items` - ); - - const batch2 = yield* addItems(["item4", "item5"]); - - yield* Effect.log( - `[BATCH 2] Added ${batch2.added} items` - ); - - const finalBatch = yield* Ref.get(batchState); - - yield* Effect.log( - `[RESULT] Total items: ${finalBatch.items.length}, Version: ${finalBatch.version}` - ); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Shared mutable state without synchronization causes problems: - -**Problem 1: Data races** -- Fiber A reads counter (value: 5) -- Fiber B reads counter (value: 5) -- Fiber A writes counter + 1 (value: 6) -- Fiber B writes counter + 1 (value: 6) -- Expected: 7, Got: 6 (lost update) - -**Problem 2: Inconsistent snapshots** -- Transaction reads user.balance (100) -- User spent money elsewhere -- Transaction reads user.balance again (90) -- Now inconsistent within same transaction - -**Problem 3: Race conditions** -- Check inventory (10 items) -- Check passes -- Before purchase, inventory goes to 0 (race) -- Purchase fails, user frustrated - -**Problem 4: Deadlocks** -- Fiber A locks state, tries to acquire another -- Fiber B holds that state, tries to acquire first -- Both stuck forever - -Solutions: - -**Atomic operations**: -- Read and modify as single operation -- No intermediate states visible -- No race window -- Guaranteed consistency - -**Compare-and-set**: -- "If value is X, change to Y" (atomic) -- Fails if another fiber changed it -- Retry automatically -- No locks needed - -**Snapshot isolation**: -- Read complete snapshot -- All operations see consistent view -- Modifications build on snapshot -- Merge changes safely - ---- - ---- - -## State Management Pattern 2: Observable State with SubscriptionRef - -**Rule:** Combine Ref with PubSub to create observable state where changes trigger notifications, enabling reactive state management. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This example demonstrates observable state patterns. - -```typescript -import { Effect, Ref, PubSub, Stream } from "effect"; - -interface StateChange { - readonly previous: T; - readonly current: T; - readonly timestamp: Date; - readonly reason: string; -} - -interface Observable { - readonly get: () => Effect.Effect; - readonly set: (value: T, reason: string) => Effect.Effect; - readonly subscribe: () => Stream.Stream>; - readonly modify: (f: (current: T) => T, reason: string) => Effect.Effect; -} - -const program = Effect.gen(function* () { - console.log( - `\n[OBSERVABLE STATE] Reactive state management\n` - ); - - // Create observable - const createObservable = (initialValue: T): Effect.Effect> => - Effect.gen(function* () { - const state = yield* Ref.make(initialValue); - const changeStream = yield* PubSub.unbounded>(); - - return { - get: () => Ref.get(state), - - set: (value: T, reason: string) => - Effect.gen(function* () { - const previous = yield* Ref.get(state); - - if (previous === value) { - return; // No change - } - - yield* Ref.set(state, value); - - const change: StateChange = { - previous, - current: value, - timestamp: new Date(), - reason, - }; - - yield* PubSub.publish(changeStream, change); - }), - - subscribe: () => - PubSub.subscribe(changeStream), - - modify: (f: (current: T) => T, reason: string) => - Effect.gen(function* () { - const previous = yield* Ref.get(state); - const updated = f(previous); - - if (previous === updated) { - return; // No change - } - - yield* Ref.set(state, updated); - - const change: StateChange = { - previous, - current: updated, - timestamp: new Date(), - reason, - }; - - yield* PubSub.publish(changeStream, change); - }), - }; - }); - - // Example 1: Basic observable counter - console.log(`[1] Observable counter:\n`); - - const counter = yield* createObservable(0); - - // Subscribe to changes - const printChanges = counter.subscribe().pipe( - Stream.tap((change) => - Effect.log( - `[CHANGE] ${change.previous} → ${change.current} (${change.reason})` - ) - ), - Stream.take(5), // Limit to 5 changes for demo - Stream.runDrain - ); - - // Make changes - yield* counter.set(1, "increment"); - yield* counter.set(2, "increment"); - yield* counter.set(5, "reset"); - - // Wait for changes to be processed - yield* Effect.sleep("100 millis"); - - // Example 2: Derived state (computed values) - console.log(`\n[2] Derived state (total from items):\n`); - - interface ShoppingCart { - readonly items: Array<{ id: string; price: number }>; - readonly discount: number; - } - - const cart = yield* createObservable({ - items: [], - discount: 0, - }); - - const computeTotal = (state: ShoppingCart): number => { - const subtotal = state.items.reduce((sum, item) => sum + item.price, 0); - return subtotal * (1 - state.discount); - }; - - // Create derived observable - const total = yield* createObservable(computeTotal(yield* cart.get())); - - // Subscribe to cart changes, update total - const updateTotalOnCartChange = cart.subscribe().pipe( - Stream.tap((change) => - Effect.gen(function* () { - const newTotal = computeTotal(change.current); - - yield* total.set(newTotal, "recalculated-from-cart"); - - yield* Effect.log( - `[TOTAL] Recalculated: $${newTotal.toFixed(2)}` - ); - }) - ), - Stream.take(10), - Stream.runDrain - ); - - // Make cart changes - yield* cart.modify( - (state) => ({ - ...state, - items: [ - ...state.items, - { id: "item1", price: 19.99 }, - ], - }), - "add-item" - ); - - yield* cart.modify( - (state) => ({ - ...state, - items: [ - ...state.items, - { id: "item2", price: 29.99 }, - ], - }), - "add-item" - ); - - yield* cart.modify( - (state) => ({ - ...state, - discount: 0.1, - }), - "apply-discount" - ); - - yield* Effect.sleep("200 millis"); - - // Example 3: Effect triggering on state change - console.log(`\n[3] Effects triggered by state changes:\n`); - - type AppStatus = "idle" | "loading" | "ready" | "error"; - - const appStatus = yield* createObservable("idle"); - - // Define effects for each status - const handleStatusChange = appStatus.subscribe().pipe( - Stream.tap((change) => - Effect.gen(function* () { - yield* Effect.log( - `[STATUS] ${change.previous} → ${change.current}` - ); - - switch (change.current) { - case "loading": - yield* Effect.log(`[EFFECT] Starting loading animation`); - break; - - case "ready": - yield* Effect.log(`[EFFECT] Hiding spinner, showing content`); - break; - - case "error": - yield* Effect.log(`[EFFECT] Showing error message`); - yield* Effect.log(`[TELEMETRY] Logging error event`); - break; - - default: - yield* Effect.log(`[EFFECT] Resetting UI`); - } - }) - ), - Stream.take(6), - Stream.runDrain - ); - - // Trigger status changes - yield* appStatus.set("loading", "user-clicked"); - yield* appStatus.set("ready", "data-loaded"); - yield* appStatus.set("loading", "user-refreshed"); - yield* appStatus.set("error", "api-failed"); - - yield* Effect.sleep("200 millis"); - - // Example 4: Multi-level state aggregation - console.log(`\n[4] Aggregated state from multiple sources:\n`); - - interface UserProfile { - name: string; - email: string; - role: string; - } - - interface AppState { - user: UserProfile | null; - notifications: number; - theme: "light" | "dark"; - } - - const appState = yield* createObservable({ - user: null, - notifications: 0, - theme: "light", - }); - - // Subscribe to track changes - const trackChanges = appState.subscribe().pipe( - Stream.tap((change) => { - if (change.current.user && !change.previous.user) { - return Effect.log(`[EVENT] User logged in: ${change.current.user.name}`); - } - - if (!change.current.user && change.previous.user) { - return Effect.log(`[EVENT] User logged out`); - } - - if (change.current.notifications !== change.previous.notifications) { - return Effect.log( - `[NOTIFY] ${change.current.notifications} notifications` - ); - } - - if (change.current.theme !== change.previous.theme) { - return Effect.log(`[THEME] Switched to ${change.current.theme}`); - } - - return Effect.succeed(undefined); - }), - Stream.take(10), - Stream.runDrain - ); - - // Make changes - yield* appState.modify( - (state) => ({ - ...state, - user: { name: "Alice", email: "alice@example.com", role: "admin" }, - }), - "user-login" - ); - - yield* appState.modify( - (state) => ({ - ...state, - notifications: 5, - }), - "new-notifications" - ); - - yield* appState.modify( - (state) => ({ - ...state, - theme: "dark", - }), - "user-preference" - ); - - yield* Effect.sleep("200 millis"); - - // Example 5: State snapshot and history - console.log(`\n[5] State history tracking:\n`); - - interface HistoryEntry { - value: T; - timestamp: Date; - reason: string; - } - - const history = yield* Ref.make[]>([]); - - const trackedCounter = yield* createObservable(0); - - const trackHistory = trackedCounter.subscribe().pipe( - Stream.tap((change) => - Effect.gen(function* () { - yield* Ref.modify(history, (h) => [ - undefined, - [ - ...h, - { - value: change.current, - timestamp: change.timestamp, - reason: change.reason, - }, - ], - ]); - - yield* Effect.log( - `[HISTORY] Recorded: ${change.current} (${change.reason})` - ); - }) - ), - Stream.take(5), - Stream.runDrain - ); - - // Make changes - for (let i = 1; i <= 4; i++) { - yield* trackedCounter.set(i, `step-${i}`); - } - - yield* Effect.sleep("200 millis"); - - // Print history - const hist = yield* Ref.get(history); - - yield* Effect.log(`\n[HISTORY] ${hist.length} entries:`); - - for (const entry of hist) { - yield* Effect.log( - ` - ${entry.value} (${entry.reason})` - ); - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Passive state causes problems: - -**Problem 1: Stale UI** -- State changes in backend -- UI doesn't know -- User sees old data -- Manual refresh required - -**Problem 2: Cascading updates** -- User changes form field -- Need to update 5 other fields -- Manual imperative code -- Fragile, easy to miss one - -**Problem 3: Derived state** -- Total = sum of items -- Manual update on each item change -- Duplicate code everywhere -- Bug: total not updated when items change - -**Problem 4: Side effects** -- User enables feature -- Multiple things must happen -- Analytics, notifications, API calls -- All imperative, hard to maintain - -Solutions: - -**Observable state**: -- State change = event -- Subscribers notified -- UI binds directly -- Auto-updates - -**Reactive flows**: -- Define how state flows -- `newTotal = items.sum()` -- Automatic recalculation -- No manual updates - -**Side effect chaining**: -- When state changes to "complete" -- Send notification -- Log event -- Trigger cleanup -- All declaratively - ---- - ---- - -## Stream Pattern 1: Transform Streams with Map and Filter - -**Rule:** Use map and filter combinators to transform stream elements declaratively, creating pipelines that reshape data without materializing intermediate results. - -**Skill Level:** beginner - -**Use Cases:** streams - -### Good Example - -This example demonstrates transforming a stream of raw data through multiple stages. - -```typescript -import { Stream, Effect, Chunk } from "effect"; - -interface RawLogEntry { - readonly timestamp: string; - readonly level: string; - readonly message: string; -} - -interface ProcessedLog { - readonly date: Date; - readonly severity: "low" | "medium" | "high"; - readonly normalizedMessage: string; -} - -// Create a stream of raw log entries -const createLogStream = (): Stream.Stream => - Stream.fromIterable([ - { timestamp: "2025-12-17T09:00:00Z", level: "DEBUG", message: "App starting" }, - { timestamp: "2025-12-17T09:01:00Z", level: "INFO", message: "Connected to DB" }, - { timestamp: "2025-12-17T09:02:00Z", level: "ERROR", message: "Query timeout" }, - { timestamp: "2025-12-17T09:03:00Z", level: "DEBUG", message: "Retry initiated" }, - { timestamp: "2025-12-17T09:04:00Z", level: "WARN", message: "Connection degraded" }, - { timestamp: "2025-12-17T09:05:00Z", level: "INFO", message: "Recovered" }, - ]); - -// Transform: Parse timestamp -const parseTimestamp = (entry: RawLogEntry): RawLogEntry => ({ - ...entry, - timestamp: entry.timestamp, // Already ISO, but could parse here -}); - -// Transform: Map log level to severity -const mapSeverity = (level: string): "low" | "medium" | "high" => { - if (level === "DEBUG" || level === "INFO") return "low"; - if (level === "WARN") return "medium"; - return "high"; -}; - -// Transform: Normalize message -const normalizeMessage = (message: string): string => - message.toLowerCase().trim(); - -// Filter: Keep only important logs -const isImportant = (entry: RawLogEntry): boolean => { - return entry.level !== "DEBUG"; -}; - -// Main pipeline -const program = Effect.gen(function* () { - console.log(`\n[STREAM] Processing log stream with map/filter\n`); - - // Create and transform stream - const transformedStream = createLogStream().pipe( - // Filter: Keep only non-debug logs - Stream.filter((entry) => { - const important = isImportant(entry); - console.log( - `[FILTER] ${entry.level} → ${important ? "✓ kept" : "✗ filtered out"}` - ); - return important; - }), - - // Map: Extract date - Stream.map((entry) => { - const date = new Date(entry.timestamp); - console.log(`[MAP-1] Parsed date: ${date.toISOString()}`); - return { ...entry, parsedDate: date }; - }), - - // Map: Normalize and map severity - Stream.map((entry) => { - const processed: ProcessedLog = { - date: entry.parsedDate, - severity: mapSeverity(entry.level), - normalizedMessage: normalizeMessage(entry.message), - }; - console.log( - `[MAP-2] Transformed: ${entry.level} → ${processed.severity}` - ); - return processed; - }) - ); - - // Collect all transformed logs - const results = yield* transformedStream.pipe( - Stream.runCollect - ); - - console.log(`\n[RESULTS]`); - console.log(` Total logs: ${results.length}`); - - Chunk.forEach(results, (log) => { - console.log( - ` - [${log.severity.toUpperCase()}] ${log.date.toISOString()}: ${log.normalizedMessage}` - ); - }); -}); - -Effect.runPromise(program); -``` - -Output shows lazy evaluation and filtering: -``` -[STREAM] Processing log stream with map/filter - -[FILTER] DEBUG → ✗ filtered out -[FILTER] INFO → ✓ kept -[MAP-1] Parsed date: 2025-12-17T09:01:00.000Z -[MAP-2] Transformed: INFO → low -[FILTER] ERROR → ✓ kept -[MAP-1] Parsed date: 2025-12-17T09:02:00.000Z -[MAP-2] Transformed: ERROR → high -... - -[RESULTS] - Total logs: 5 - - [LOW] 2025-12-17T09:01:00.000Z: connected to db - - [HIGH] 2025-12-17T09:02:00.000Z: query timeout - ... -``` - ---- - -### Explanation - -Streaming data transformations without map/filter create problems: - -- **Buffering**: Must collect all data before transforming -- **Code verbosity**: Manual loops for each transformation -- **Memory usage**: Large intermediate arrays -- **Composability**: Hard to chain operations - -Map/filter enable: - -- **Lazy evaluation**: Transform on-demand -- **Composable**: Chain operations naturally -- **Memory efficient**: No intermediate collections -- **Expressive**: Declare intent clearly - -Real-world example: Processing logs -- **Without map/filter**: Collect logs, filter by level, map to objects, transform fields -- **With map/filter**: `logStream.pipe(Stream.filter(...), Stream.map(...))` - ---- - ---- - -## Stream Pattern 2: Merge and Combine Multiple Streams - -**Rule:** Use merge and concat combinators to combine multiple streams, enabling aggregation of data from multiple independent sources. - -**Skill Level:** intermediate - -**Use Cases:** streams - -### Good Example - -This example demonstrates merging multiple event streams into a unified stream. - -```typescript -import { Stream, Effect, Chunk } from "effect"; - -interface Event { - readonly source: string; - readonly type: string; - readonly data: string; - readonly timestamp: Date; -} - -// Create independent event streams from different sources -const createUserEventStream = (): Stream.Stream => - Stream.fromIterable([ - { source: "user-service", type: "login", data: "user-123", timestamp: new Date(Date.now() + 0) }, - { source: "user-service", type: "logout", data: "user-123", timestamp: new Date(Date.now() + 500) }, - ]).pipe( - Stream.tap(() => Effect.sleep("500 millis")) - ); - -const createPaymentEventStream = (): Stream.Stream => - Stream.fromIterable([ - { source: "payment-service", type: "payment-started", data: "order-456", timestamp: new Date(Date.now() + 200) }, - { source: "payment-service", type: "payment-completed", data: "order-456", timestamp: new Date(Date.now() + 800) }, - ]).pipe( - Stream.tap(() => Effect.sleep("600 millis")) - ); - -const createAuditEventStream = (): Stream.Stream => - Stream.fromIterable([ - { source: "audit-log", type: "access-granted", data: "resource-789", timestamp: new Date(Date.now() + 100) }, - { source: "audit-log", type: "access-revoked", data: "resource-789", timestamp: new Date(Date.now() + 900) }, - ]).pipe( - Stream.tap(() => Effect.sleep("800 millis")) - ); - -// Merge streams (interleaved, unordered) -const mergedEventStream = (): Stream.Stream => { - const userStream = createUserEventStream(); - const paymentStream = createPaymentEventStream(); - const auditStream = createAuditEventStream(); - - return Stream.merge(userStream, paymentStream, auditStream); -}; - -// Concat streams (sequential, ordered) -const concatenatedEventStream = (): Stream.Stream => { - return createUserEventStream().pipe( - Stream.concat(createPaymentEventStream()), - Stream.concat(createAuditEventStream()) - ); -}; - -// Main: Compare merge vs concat -const program = Effect.gen(function* () { - console.log(`\n[MERGE] Interleaved events from multiple sources:\n`); - - // Collect merged stream - const mergedEvents = yield* mergedEventStream().pipe( - Stream.runCollect - ); - - Chunk.forEach(mergedEvents, (event, idx) => { - console.log( - ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}` - ); - }); - - console.log(`\n[CONCAT] Sequential events (user → payment → audit):\n`); - - // Collect concatenated stream - const concatEvents = yield* concatenatedEventStream().pipe( - Stream.runCollect - ); - - Chunk.forEach(concatEvents, (event, idx) => { - console.log( - ` ${idx + 1}. [${event.source}] ${event.type}: ${event.data}` - ); - }); -}); - -Effect.runPromise(program); -``` - -Output shows merge interleaving vs concat ordering: -``` -[MERGE] Interleaved events from multiple sources: - - 1. [audit-log] access-granted: resource-789 - 2. [user-service] login: user-123 - 3. [payment-service] payment-started: order-456 - 4. [user-service] logout: user-123 - 5. [payment-service] payment-completed: order-456 - 6. [audit-log] access-revoked: resource-789 - -[CONCAT] Sequential events (user → payment → audit): - - 1. [user-service] login: user-123 - 2. [user-service] logout: user-123 - 3. [payment-service] payment-started: order-456 - 4. [payment-service] payment-completed: order-456 - 5. [audit-log] access-granted: resource-789 - 6. [audit-log] access-revoked: resource-789 -``` - ---- - -### Explanation - -Multi-source data processing without merge/concat creates issues: - -- **Complex coordination**: Manual loop over multiple sources -- **Hard to aggregate**: Collecting from different sources is verbose -- **Ordering confusion**: Sequential vs. parallel unclear -- **Resource management**: Multiple independent consumers - -Merge/concat enable: - -- **Simple composition**: Combine streams naturally -- **Semantic clarity**: Merge = parallel, concat = sequential -- **Aggregation**: Single consumer for multiple sources -- **Scalability**: Add sources without refactoring - -Real-world example: Aggregating user events -- **Without merge**: Poll user service, poll event log, poll notifications separately -- **With merge**: `Stream.merge(userStream, eventStream, notificationStream)` - ---- - ---- - -## Stream Pattern 3: Control Backpressure in Streams - -**Rule:** Use backpressure control to manage flow between fast producers and slow consumers, preventing memory exhaustion and resource overflow. - -**Skill Level:** intermediate - -**Use Cases:** streams - -### Good Example - -This example demonstrates managing backpressure when consuming events at different rates. - -```typescript -import { Stream, Effect, Chunk } from "effect"; - -interface DataPoint { - readonly id: number; - readonly value: number; -} - -// Fast producer: generates 100 items per second -const fastProducer = (): Stream.Stream => - Stream.fromIterable(Array.from({ length: 100 }, (_, i) => ({ id: i, value: Math.random() }))).pipe( - Stream.tap(() => Effect.sleep("10 millis")) // 10ms per item = 100/sec - ); - -// Slow consumer: processes 10 items per second -const slowConsumer = (item: DataPoint): Effect.Effect => - Effect.gen(function* () { - yield* Effect.sleep("100 millis"); // 100ms per item = 10/sec - }); - -// Without backpressure (DANGEROUS - queue grows unbounded) -const unbufferedStream = (): Stream.Stream => - fastProducer().pipe( - Stream.tap((item) => - Effect.log(`[UNBUFFERED] Produced item ${item.id}`) - ) - ); - -// With bounded buffer (backpressure kicks in) -const bufferedStream = (bufferSize: number): Stream.Stream => - fastProducer().pipe( - // Buffer at most 10 items; if full, producer waits - Stream.buffer(bufferSize), - Stream.tap((item) => - Effect.log(`[BUFFERED] Consumed item ${item.id}`) - ) - ); - -// With throttling (rate limit emission) -const throttledStream = (): Stream.Stream => - fastProducer().pipe( - // Emit at most 1 item per 50ms (20/sec) - Stream.throttle(1, "50 millis"), - Stream.tap((item) => - Effect.log(`[THROTTLED] Item ${item.id}`) - ) - ); - -// Main: compare approaches -const program = Effect.gen(function* () { - console.log(`\n[START] Demonstrating backpressure management\n`); - - // Test buffered approach - console.log(`[TEST 1] Buffered stream (buffer size 5):\n`); - - const startBuffer = Date.now(); - - yield* bufferedStream(5).pipe( - Stream.take(20), // Take only 20 items - Stream.runForEach(slowConsumer) - ); - - const bufferTime = Date.now() - startBuffer; - console.log(`\n[RESULT] Buffered approach took ${bufferTime}ms\n`); - - // Test throttled approach - console.log(`[TEST 2] Throttled stream (1 item per 50ms):\n`); - - const startThrottle = Date.now(); - - yield* throttledStream().pipe( - Stream.take(20), - Stream.runForEach(slowConsumer) - ); - - const throttleTime = Date.now() - startThrottle; - console.log(`\n[RESULT] Throttled approach took ${throttleTime}ms\n`); - - // Summary - console.log(`[SUMMARY]`); - console.log(` Without backpressure control:`); - console.log(` - Queue would grow to 100 items (memory risk)`); - console.log(` - Producer/consumer operate independently`); - console.log(` With buffering:`); - console.log(` - Queue bounded to 5 items (safe)`); - console.log(` - Producer waits when buffer full`); - console.log(` With throttling:`); - console.log(` - Production rate limited to 20/sec`); - console.log(` - Smooth controlled flow`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Without backpressure management, mismatched producer/consumer speeds cause: - -- **Memory exhaustion**: Producer faster than consumer → queue grows unbounded -- **Garbage collection pauses**: Large buffers cause GC pressure -- **Resource leaks**: Open connections/file handles accumulate -- **Cascade failures**: One slow consumer blocks entire pipeline - -Backpressure enable: - -- **Memory safety**: Bounded buffers prevent overflow -- **Resource efficiency**: Consumers pace producers naturally -- **Performance**: Tuning buffer sizes improves throughput -- **Observability**: Monitor backpressure as health indicator - -Real-world example: Reading large file vs. writing to database -- **No backpressure**: Read entire file into memory, write slowly → memory exhaustion -- **With backpressure**: Read 1000 lines, wait for database, read next batch - ---- - ---- - -## Stream Pattern 4: Stateful Operations with Scan and Fold - -**Rule:** Use scan for stateful element-by-element processing and fold for final aggregation, enabling complex stream analytics without buffering entire stream. - -**Skill Level:** intermediate - -**Use Cases:** streams - -### Good Example - -This example demonstrates maintaining statistics across a stream of measurements. - -```typescript -import { Stream, Effect, Chunk } from "effect"; - -interface Measurement { - readonly id: number; - readonly value: number; - readonly timestamp: Date; -} - -interface RunningStats { - readonly count: number; - readonly sum: number; - readonly min: number; - readonly max: number; - readonly average: number; - readonly variance: number; - readonly lastValue: number; -} - -// Create stream of measurements -const createMeasurementStream = (): Stream.Stream => - Stream.fromIterable([ - { id: 1, value: 10, timestamp: new Date() }, - { id: 2, value: 20, timestamp: new Date() }, - { id: 3, value: 15, timestamp: new Date() }, - { id: 4, value: 25, timestamp: new Date() }, - { id: 5, value: 30, timestamp: new Date() }, - { id: 6, value: 22, timestamp: new Date() }, - ]); - -// Initial statistics state -const initialStats: RunningStats = { - count: 0, - sum: 0, - min: Infinity, - max: -Infinity, - average: 0, - variance: 0, - lastValue: 0, -}; - -// Reducer: update stats for each measurement -const updateStats = ( - stats: RunningStats, - measurement: Measurement -): RunningStats => { - const newCount = stats.count + 1; - const newSum = stats.sum + measurement.value; - const newAverage = newSum / newCount; - - // Calculate variance incrementally - const delta = measurement.value - stats.average; - const delta2 = measurement.value - newAverage; - const newVariance = stats.variance + delta * delta2; - - return { - count: newCount, - sum: newSum, - min: Math.min(stats.min, measurement.value), - max: Math.max(stats.max, measurement.value), - average: newAverage, - variance: newVariance / newCount, - lastValue: measurement.value, - }; -}; - -// Main: demonstrate scan with statistics -const program = Effect.gen(function* () { - console.log(`\n[SCAN] Running statistics stream:\n`); - - // Use scan to emit intermediate statistics - const statsStream = createMeasurementStream().pipe( - Stream.scan(initialStats, (stats, measurement) => { - const newStats = updateStats(stats, measurement); - - console.log( - `[MEASUREMENT ${measurement.id}] Value: ${measurement.value}` - ); - console.log( - ` Count: ${newStats.count}, Avg: ${newStats.average.toFixed(2)}, ` + - `Min: ${newStats.min}, Max: ${newStats.max}, ` + - `Variance: ${newStats.variance.toFixed(2)}` - ); - - return newStats; - }) - ); - - // Collect all intermediate stats - const allStats = yield* statsStream.pipe(Stream.runCollect); - - // Final statistics - const finalStats = Chunk.last(allStats); - - if (finalStats._tag === "Some") { - console.log(`\n[FINAL STATISTICS]`); - console.log(` Total measurements: ${finalStats.value.count}`); - console.log(` Average: ${finalStats.value.average.toFixed(2)}`); - console.log(` Min: ${finalStats.value.min}`); - console.log(` Max: ${finalStats.value.max}`); - console.log( - ` Std Dev: ${Math.sqrt(finalStats.value.variance).toFixed(2)}` - ); - } - - // Compare with fold (emit only final result) - console.log(`\n[FOLD] Final statistics only:\n`); - - const finalResult = yield* createMeasurementStream().pipe( - Stream.fold(initialStats, updateStats), - Stream.tap((stats) => - Effect.log(`Final: Count=${stats.count}, Avg=${stats.average.toFixed(2)}`) - ) - ); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Processing streams without scan/fold creates issues: - -- **Manual state tracking**: Ref or mutable variables outside stream -- **Lost context**: Hard to correlate intermediate values -- **Error-prone**: Easy to forget state updates -- **Testing difficulty**: State spread across code - -Scan/fold enable: - -- **Declarative state**: State threaded through stream -- **Intermediate values**: Emit state at each step (scan) -- **Type-safe**: Accumulator type guaranteed -- **Composable**: Chain stateful operations - -Real-world example: Running average of metrics -- **Without scan**: Track count and sum manually, calculate average, emit -- **With scan**: `stream.pipe(Stream.scan(initialState, updateAverage))` - ---- - ---- - -## Stream Pattern 5: Grouping and Windowing Streams - -**Rule:** Use groupBy to partition streams by key and tumbling/sliding windows to aggregate streams over time windows. - -**Skill Level:** advanced - -**Use Cases:** streams - -### Good Example - -This example demonstrates windowing and grouping patterns. - -```typescript -import { Effect, Stream, Ref, Duration, Schedule } from "effect"; - -interface Event { - readonly timestamp: Date; - readonly userId: string; - readonly action: string; - readonly duration: number; // milliseconds -} - -// Simulate event stream -const generateEvents = (): Event[] => [ - { timestamp: new Date(Date.now() - 5000), userId: "user1", action: "click", duration: 100 }, - { timestamp: new Date(Date.now() - 4500), userId: "user2", action: "view", duration: 250 }, - { timestamp: new Date(Date.now() - 4000), userId: "user1", action: "scroll", duration: 150 }, - { timestamp: new Date(Date.now() - 3500), userId: "user3", action: "click", duration: 120 }, - { timestamp: new Date(Date.now() - 3000), userId: "user2", action: "click", duration: 180 }, - { timestamp: new Date(Date.now() - 2500), userId: "user1", action: "view", duration: 200 }, - { timestamp: new Date(Date.now() - 2000), userId: "user3", action: "view", duration: 300 }, - { timestamp: new Date(Date.now() - 1500), userId: "user1", action: "submit", duration: 500 }, - { timestamp: new Date(Date.now() - 1000), userId: "user2", action: "scroll", duration: 100 }, -]; - -// Main: windowing and grouping examples -const program = Effect.gen(function* () { - console.log(`\n[WINDOWING & GROUPING] Stream organization patterns\n`); - - const events = generateEvents(); - - // Example 1: Tumbling window (fixed-size batches) - console.log(`[1] Tumbling window (2-event batches):\n`); - - const windowSize = 2; - let batchNumber = 1; - - for (let i = 0; i < events.length; i += windowSize) { - const batch = events.slice(i, i + windowSize); - - yield* Effect.log(`[WINDOW ${batchNumber}] (${batch.length} events)`); - - let totalDuration = 0; - - for (const event of batch) { - yield* Effect.log( - ` - ${event.userId}: ${event.action} (${event.duration}ms)` - ); - - totalDuration += event.duration; - } - - yield* Effect.log(`[WINDOW ${batchNumber}] Total duration: ${totalDuration}ms\n`); - - batchNumber++; - } - - // Example 2: Sliding window (overlapping) - console.log(`[2] Sliding window (last 3 events, slide by 1):\n`); - - const windowSizeSlide = 3; - const slideBy = 1; - - for (let i = 0; i <= events.length - windowSizeSlide; i += slideBy) { - const window = events.slice(i, i + windowSizeSlide); - - const avgDuration = - window.reduce((sum, e) => sum + e.duration, 0) / window.length; - - yield* Effect.log( - `[SLIDE ${i / slideBy}] ${window.length} events, avg duration: ${avgDuration.toFixed(0)}ms` - ); - } - - // Example 3: Group by key - console.log(`\n[3] Group by user:\n`); - - const byUser = new Map(); - - for (const event of events) { - if (!byUser.has(event.userId)) { - byUser.set(event.userId, []); - } - - byUser.get(event.userId)!.push(event); - } - - for (const [userId, userEvents] of byUser) { - const totalActions = userEvents.length; - const totalTime = userEvents.reduce((sum, e) => sum + e.duration, 0); - const avgTime = totalTime / totalActions; - - yield* Effect.log( - `[USER ${userId}] ${totalActions} actions, ${totalTime}ms total, ${avgTime.toFixed(0)}ms avg` - ); - } - - // Example 4: Group + Window combination - console.log(`\n[4] Group by user, window by action type:\n`); - - for (const [userId, userEvents] of byUser) { - const byAction = new Map(); - - for (const event of userEvents) { - if (!byAction.has(event.action)) { - byAction.set(event.action, []); - } - - byAction.get(event.action)!.push(event); - } - - yield* Effect.log(`[USER ${userId}] Action breakdown:`); - - for (const [action, actionEvents] of byAction) { - const count = actionEvents.length; - const total = actionEvents.reduce((sum, e) => sum + e.duration, 0); - - yield* Effect.log(` ${action}: ${count}x (${total}ms total)`); - } - } - - // Example 5: Session window (based on inactivity timeout) - console.log(`\n[5] Session window (gap > 1000ms = new session):\n`); - - const sessionGapMs = 1000; - const sessions: Event[][] = []; - let currentSession: Event[] = []; - let lastTimestamp = events[0]?.timestamp.getTime() ?? 0; - - for (const event of events) { - const currentTime = event.timestamp.getTime(); - const timeSinceLastEvent = currentTime - lastTimestamp; - - if (timeSinceLastEvent > sessionGapMs && currentSession.length > 0) { - sessions.push(currentSession); - yield* Effect.log( - `[SESSION] Closed (${currentSession.length} events, gap: ${timeSinceLastEvent}ms)` - ); - - currentSession = []; - } - - currentSession.push(event); - lastTimestamp = currentTime; - } - - if (currentSession.length > 0) { - sessions.push(currentSession); - yield* Effect.log(`[SESSION] Final (${currentSession.length} events)`); - } - - // Example 6: Top-K aggregation in window - console.log(`\n[6] Top 2 actions in last window:\n`); - - const lastWindow = events.slice(-3); - - const actionCounts = new Map(); - - for (const event of lastWindow) { - actionCounts.set( - event.action, - (actionCounts.get(event.action) ?? 0) + 1 - ); - } - - const topActions = Array.from(actionCounts.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 2); - - yield* Effect.log(`[TOP-K] In last window of 3 events:`); - - for (const [action, count] of topActions) { - yield* Effect.log(` ${action}: ${count}x`); - } -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Unbounded streams need boundaries: - -**Problem 1: Memory exhaustion** -- Processing 1M events with no boundary = keep all in memory -- Cumulative memory grows unbounded -- Eventually OOM error - -**Problem 2: Late aggregation** -- Can't sum all events until stream ends (never) -- Need to decide: "sum events in this 1-second window" - -**Problem 3: Grouping complexity** -- Stream of user events: need per-user aggregation -- Without groupBy: manual state tracking (error-prone) - -**Problem 4: Temporal patterns** -- "Top 10 searches in last 5 minutes" requires windowing -- "Average response time per endpoint per minute" requires grouping + windowing - -Solutions: - -**Tumbling window**: -- Divide stream into 1-sec, 5-sec, or 1-min chunks -- Process each chunk independently -- Clear memory between windows -- Natural for: metrics, batching, reports - -**Sliding window**: -- Keep last 5 minutes of data at all times -- Emit updated aggregation every second -- Detect patterns over overlapping periods -- Natural for: anomaly detection, trends - -**Group by**: -- Separate streams by key -- Each key has independent state -- Emit grouped results -- Natural for: per-user, per-endpoint, per-tenant - ---- - ---- - -## Stream Pattern 6: Resource Management in Streams - -**Rule:** Use Stream.bracket or effect scoping to guarantee resource cleanup, preventing leaks even when streams fail or are interrupted. - -**Skill Level:** advanced - -**Use Cases:** streams - -### Good Example - -This example demonstrates resource acquisition, use, and guaranteed cleanup. - -```typescript -import { Effect, Stream, Resource, Scope, Ref } from "effect"; - -interface FileHandle { - readonly path: string; - readonly fd: number; -} - -interface Connection { - readonly id: string; - readonly isOpen: boolean; -} - -// Simulate resource management -const program = Effect.gen(function* () { - console.log(`\n[RESOURCE MANAGEMENT] Stream resource lifecycle\n`); - - // Example 1: Bracket pattern for file streams - console.log(`[1] Bracket pattern (acquire → use → release):\n`); - - let openHandles = 0; - let closedHandles = 0; - - const openFile = (path: string) => - Effect.gen(function* () { - openHandles++; - yield* Effect.log(`[OPEN] File "${path}" (total open: ${openHandles})`); - - return { path, fd: 1000 + openHandles }; - }); - - const closeFile = (handle: FileHandle) => - Effect.gen(function* () { - closedHandles++; - yield* Effect.log(`[CLOSE] File "${handle.path}" (total closed: ${closedHandles})`); - }); - - const readFileWithBracket = (path: string) => - Effect.gen(function* () { - let handle: FileHandle | null = null; - - try { - handle = yield* openFile(path); - - yield* Effect.log( - `[USE] Reading from fd ${handle.fd} ("${handle.path}")` - ); - - // Simulate reading - return "file contents"; - } finally { - // Guaranteed to run even if error occurs above - if (handle) { - yield* closeFile(handle); - } - } - }); - - // Test with success - yield* Effect.log(`[TEST] Success case:`); - - const content = yield* readFileWithBracket("/data/file.txt"); - - yield* Effect.log(`[RESULT] Got: "${content}"\n`); - - // Test with failure (simulated) - yield* Effect.log(`[TEST] Error case:`); - - const failCase = Effect.gen(function* () { - let handle: FileHandle | null = null; - - try { - handle = yield* openFile("/data/missing.txt"); - - // Simulate error mid-operation - yield* Effect.fail(new Error("Read failed")); - } finally { - if (handle) { - yield* closeFile(handle); - } - } - }).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[ERROR] Caught: ${error.message}`); - yield* Effect.log(`[CHECK] Closed handles: ${closedHandles} (verifying cleanup)\n`); - }) - ) - ); - - yield* failCase; - - // Example 2: Connection pool management - console.log(`[2] Connection pooling:\n`); - - interface ConnectionPool { - acquire: () => Effect.Effect; - release: (conn: Connection) => Effect.Effect; - } - - const createConnectionPool = (maxSize: number): Effect.Effect => - Effect.gen(function* () { - const available = yield* Ref.make([]); - const inUse = yield* Ref.make>(new Set()); - let idCounter = 0; - - return { - acquire: Effect.gen(function* () { - const avail = yield* Ref.get(available); - - if (avail.length > 0) { - yield* Effect.log(`[POOL] Reusing connection from pool`); - - const conn = avail.pop()!; - - yield* Ref.modify(inUse, (set) => [ - undefined, - new Set(set).add(conn.id), - ]); - - return conn; - } - - const inUseCount = (yield* Ref.get(inUse)).size; - - if (inUseCount >= maxSize) { - yield* Effect.fail(new Error("Pool exhausted")); - } - - const connId = `conn-${++idCounter}`; - - yield* Effect.log(`[POOL] Creating new connection: ${connId}`); - - const conn = { id: connId, isOpen: true }; - - yield* Ref.modify(inUse, (set) => [ - undefined, - new Set(set).add(connId), - ]); - - return conn; - }), - - release: (conn: Connection) => - Effect.gen(function* () { - yield* Ref.modify(inUse, (set) => { - const updated = new Set(set); - updated.delete(conn.id); - return [undefined, updated]; - }); - - yield* Ref.modify(available, (avail) => [ - undefined, - [...avail, conn], - ]); - - yield* Effect.log(`[POOL] Returned connection: ${conn.id}`); - }), - }; - }); - - const pool = yield* createConnectionPool(3); - - // Acquire and release connections - const conn1 = yield* pool.acquire(); - const conn2 = yield* pool.acquire(); - - yield* pool.release(conn1); - - const conn3 = yield* pool.acquire(); // Reuses conn1 - - yield* Effect.log(`\n`); - - // Example 3: Scope-based resource safety - console.log(`[3] Scoped resources (hierarchical cleanup):\n`); - - let scopedCount = 0; - - const withScoped = (create: () => Effect.Effect) => - Effect.gen(function* () { - scopedCount++; - const id = scopedCount; - - yield* Effect.log(`[SCOPE] Enter scope ${id}`); - - const resource = yield* create(); - - yield* Effect.log(`[SCOPE] Using resource in scope ${id}`); - - yield* Effect.sync(() => { - // Cleanup happens here when scope exits - yield* Effect.log(`[SCOPE] Exit scope ${id}`); - }).pipe( - Effect.ensuring( - Effect.log(`[SCOPE] Cleanup guaranteed for scope ${id}`) - ) - ); - - return resource; - }); - - // Nested scopes - const result = yield* withScoped(() => - Effect.succeed({ - level: 1, - data: yield* withScoped(() => Effect.succeed("inner data")), - }) - ).pipe( - Effect.catchAll(() => Effect.succeed({ level: 0, data: null })) - ); - - yield* Effect.log(`[SCOPES] Cleanup order: inner → outer\n`); - - // Example 4: Stream resource management - console.log(`[4] Stream with resource cleanup:\n`); - - let streamResourceCount = 0; - - // Simulate stream that acquires resources - const streamWithResources = Stream.empty.pipe( - Stream.tap(() => - Effect.gen(function* () { - streamResourceCount++; - yield* Effect.log(`[STREAM-RES] Acquired resource ${streamResourceCount}`); - }) - ), - // Cleanup when stream ends - Stream.ensuring( - Effect.log(`[STREAM-RES] Cleaning up all ${streamResourceCount} resources`) - ) - ); - - yield* Stream.runDrain(streamWithResources); - - // Example 5: Error propagation with cleanup - console.log(`\n[5] Error safety with cleanup:\n`); - - const safeRead = (retryCount: number) => - Effect.gen(function* () { - let handle: FileHandle | null = null; - - try { - handle = yield* openFile(`/data/file-${retryCount}.txt`); - - if (retryCount < 2) { - yield* Effect.log(`[READ] Attempt ${retryCount}: failing intentionally`); - yield* Effect.fail(new Error(`Attempt ${retryCount} failed`)); - } - - yield* Effect.log(`[READ] Success on attempt ${retryCount}`); - - return "success"; - } finally { - if (handle) { - yield* closeFile(handle); - } - } - }); - - // Retry with guaranteed cleanup - const result2 = yield* safeRead(1).pipe( - Effect.retry( - Schedule.recurs(2).pipe( - Schedule.compose(Schedule.fixed("10 millis")) - ) - ), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[FINAL] All retries failed: ${error.message}`); - return "fallback"; - }) - ) - ); - - yield* Effect.log(`\n[FINAL] Result: ${result2}`); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Streams without resource management cause problems: - -**Problem 1: Resource exhaustion** -- Open file streams without closing → file descriptor limit exceeded -- Get connections from pool, never return → connection starvation -- System becomes unresponsive - -**Problem 2: Memory leaks** -- Stream emits large objects → memory grows -- Without cleanup → garbage persists -- GC can't reclaim - -**Problem 3: Data corruption** -- Write to file without flush → partial writes on crash -- Read from connection while another thread writes → data race -- Results are unpredictable - -**Problem 4: Silent failures** -- Resource cleanup fails → error lost -- Application proceeds as if successful -- Hidden bug becomes hard-to-trace crash later - -Solutions: - -**Bracket pattern**: -- Acquire resource -- Use resource (even if error) -- Always release resource -- Track errors separately - -**Resource scopes**: -- Nested resource management -- Parent cleanup waits for children -- Hierarchical resource graphs -- Type-safe guarantees - -**Connection pooling**: -- Reuse connections -- Track available/in-use -- Prevent exhaustion -- Support graceful shutdown - ---- - ---- - -## Stream Pattern 7: Error Handling in Streams - -**Rule:** Use Stream error handlers to recover from failures, retry operations, and maintain stream integrity even when individual elements fail. - -**Skill Level:** advanced - -**Use Cases:** streams - -### Good Example - -This example demonstrates stream error handling patterns. - -```typescript -import { Effect, Stream, Ref } from "effect"; - -interface DataRecord { - id: string; - value: number; -} - -interface ProcessingResult { - successful: DataRecord[]; - failed: Array<{ id: string; error: string }>; - retried: number; -} - -const program = Effect.gen(function* () { - console.log(`\n[STREAM ERROR HANDLING] Resilient stream processing\n`); - - // Example 1: Continue on error (skip failed, process rest) - console.log(`[1] Continue processing despite errors:\n`); - - const processElement = (record: DataRecord): Effect.Effect => - Effect.gen(function* () { - if (record.value < 0) { - yield* Effect.fail(new Error(`Invalid value: ${record.value}`)); - } - - return `processed-${record.id}`; - }); - - const records = [ - { id: "rec1", value: 10 }, - { id: "rec2", value: -5 }, // Will fail - { id: "rec3", value: 20 }, - { id: "rec4", value: -1 }, // Will fail - { id: "rec5", value: 30 }, - ]; - - const successfulProcessing = yield* Stream.fromIterable(records).pipe( - Stream.mapEffect((record) => - processElement(record).pipe( - Effect.map((result) => ({ success: true, result })), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[ERROR] Record ${record.id} failed`); - - return { success: false, error }; - }) - ) - ) - ), - Stream.runCollect - ); - - yield* Effect.log( - `[RESULTS] ${successfulProcessing.filter((r) => r.success).length}/${records.length} succeeded\n` - ); - - // Example 2: Recover with fallback value - console.log(`[2] Providing fallback on error:\n`); - - const getData = (id: string): Effect.Effect => - id.includes("fail") ? Effect.fail(new Error("Data error")) : Effect.succeed(42); - - const recovered = yield* Stream.fromIterable(["ok1", "fail1", "ok2"]).pipe( - Stream.mapEffect((id) => - getData(id).pipe( - Effect.catchAll(() => - Effect.gen(function* () { - yield* Effect.log(`[FALLBACK] Using default for ${id}`); - - return -1; // Fallback value - }) - ) - ) - ), - Stream.runCollect - ); - - yield* Effect.log(`[VALUES] ${recovered.join(", ")}\n`); - - // Example 3: Collect errors alongside successes - console.log(`[3] Collecting errors and successes:\n`); - - const results = yield* Ref.make({ - successful: [], - failed: [], - retried: 0, - }); - - yield* Stream.fromIterable(records).pipe( - Stream.mapEffect((record) => - processElement(record).pipe( - Effect.tap((result) => - Ref.modify(results, (r) => [ - undefined, - { - ...r, - successful: [...r.successful, record], - }, - ]) - ), - Effect.catchAll((error) => - Ref.modify(results, (r) => [ - undefined, - { - ...r, - failed: [ - ...r.failed, - { id: record.id, error: error.message }, - ], - }, - ]) - ) - ) - ), - Stream.runDrain - ); - - const finalResults = yield* Ref.get(results); - - yield* Effect.log( - `[AGGREGATE] ${finalResults.successful.length} succeeded, ${finalResults.failed.length} failed` - ); - - for (const failure of finalResults.failed) { - yield* Effect.log(` - ${failure.id}: ${failure.error}`); - } - - // Example 4: Retry on error with backoff - console.log(`\n[4] Retry with exponential backoff:\n`); - - let attemptCount = 0; - - const unreliableOperation = (id: string): Effect.Effect => - Effect.gen(function* () { - attemptCount++; - - if (attemptCount <= 2) { - yield* Effect.log(`[ATTEMPT ${attemptCount}] Failing for ${id}`); - - yield* Effect.fail(new Error("Temporary failure")); - } - - yield* Effect.log(`[SUCCESS] Succeeded on attempt ${attemptCount}`); - - return `result-${id}`; - }); - - const retried = unreliableOperation("test").pipe( - Effect.retry( - Schedule.exponential("10 millis").pipe( - Schedule.upTo("100 millis"), - Schedule.recurs(3) - ) - ), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[EXHAUSTED] All retries failed`); - - return "fallback"; - }) - ) - ); - - yield* retried; - - // Example 5: Error context in streams - console.log(`\n[5] Propagating error context:\n`); - - interface StreamContext { - batchId: string; - timestamp: Date; - } - - const processWithContext = (context: StreamContext) => - Stream.fromIterable([1, 2, -3, 4]).pipe( - Stream.mapEffect((value) => - Effect.gen(function* () { - if (value < 0) { - yield* Effect.fail( - new Error( - `Negative value in batch ${context.batchId} at ${context.timestamp.toISOString()}` - ) - ); - } - - return value * 2; - }) - ), - Stream.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[CONTEXT ERROR] ${error.message}`); - - return Stream.empty; - }) - ) - ); - - const context: StreamContext = { - batchId: "batch-001", - timestamp: new Date(), - }; - - yield* processWithContext(context).pipe(Stream.runDrain); - - // Example 6: Partial recovery (keep good data, log bad) - console.log(`\n[6] Partial recovery strategy:\n`); - - const mixedQuality = [ - { id: "1", data: "good" }, - { id: "2", data: "bad" }, - { id: "3", data: "good" }, - { id: "4", data: "bad" }, - { id: "5", data: "good" }, - ]; - - const processQuality = (record: { id: string; data: string }) => - record.data === "good" - ? Effect.succeed(`valid-${record.id}`) - : Effect.fail(new Error(`Invalid data for ${record.id}`)); - - const partialResults = yield* Stream.fromIterable(mixedQuality).pipe( - Stream.mapEffect((record) => - processQuality(record).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[LOG] ${error.message}`); - - return null; // Skip this record - }) - ) - ) - ), - Stream.filter((result) => result !== null), - Stream.runCollect - ); - - yield* Effect.log( - `[PARTIAL] Kept ${partialResults.length}/${mixedQuality.length} valid records\n` - ); - - // Example 7: Timeout handling in streams - console.log(`[7] Timeout handling per element:\n`); - - const slowOperation = (id: string): Effect.Effect => - Effect.gen(function* () { - // Simulate slow operations - if (id === "slow") { - yield* Effect.sleep("200 millis"); - } else { - yield* Effect.sleep("50 millis"); - } - - return `done-${id}`; - }); - - const withTimeout = yield* Stream.fromIterable(["fast1", "slow", "fast2"]).pipe( - Stream.mapEffect((id) => - slowOperation(id).pipe( - Effect.timeout("100 millis"), - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[TIMEOUT] Operation ${id} timed out`); - - return "timeout-fallback"; - }) - ) - ) - ), - Stream.runCollect - ); - - yield* Effect.log(`[RESULTS] ${withTimeout.join(", ")}\n`); - - // Example 8: Stream termination on critical error - console.log(`[8] Terminating stream on critical error:\n`); - - const isCritical = (error: Error): boolean => - error.message.includes("CRITICAL"); - - const terminateOnCritical = Stream.fromIterable([1, 2, 3]).pipe( - Stream.mapEffect((value) => - value === 2 - ? Effect.fail(new Error("CRITICAL: System failure")) - : Effect.succeed(value) - ), - Stream.catchAll((error) => - Effect.gen(function* () { - if (isCritical(error)) { - yield* Effect.log(`[CRITICAL] Terminating stream`); - - return Stream.fail(error); - } - - yield* Effect.log(`[WARNING] Continuing despite error`); - - return Stream.empty; - }) - ) - ); - - yield* terminateOnCritical.pipe( - Stream.runCollect, - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[STOPPED] Stream stopped: ${error.message}`); - - return []; - }) - ) - ); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Errors in streams cause cascading failures: - -**Problem 1: Stream death** -- Process 10,000 records -- Record #5000 has bad data -- Stream crashes -- 9,000 records not processed -- Manual re-run needed - -**Problem 2: Silent data loss** -- Stream encounters error -- Stops processing -- Caller doesn't notice -- Missing data goes undetected -- Reports wrong numbers - -**Problem 3: No recovery visibility** -- Error happens -- Is it retried? How many times? -- Did it recover? -- Silent guessing required - -**Problem 4: Downstream effects** -- Stream error affects all subscribers -- Cascading failure -- System becomes unavailable -- All downstream blocked - -Solutions: - -**Continue on error**: -- Skip failed element -- Process rest of stream -- Collect error for later -- Partial success acceptable - -**Retry with backoff**: -- Transient error? Retry -- Exponential backoff -- Eventually give up -- Move to next element - -**Error aggregation**: -- Collect all errors -- Collect all successes -- Report both -- Analytics/debugging - -**Graceful termination**: -- Signal end of stream on error -- Allow cleanup -- Prevent resource leak -- Controlled shutdown - ---- - ---- - -## Stream Pattern 8: Advanced Stream Transformations - -**Rule:** Use advanced stream operators to build sophisticated data pipelines that compose elegantly and maintain performance at scale. - -**Skill Level:** advanced - -**Use Cases:** streams - -### Good Example - -This example demonstrates advanced stream transformations. - -```typescript -import { Effect, Stream, Ref, Chunk } from "effect"; - -interface LogEntry { - timestamp: Date; - level: "info" | "warn" | "error"; - message: string; - context?: Record; -} - -interface Metric { - name: string; - value: number; - tags: Record; -} - -const program = Effect.gen(function* () { - console.log(`\n[ADVANCED STREAM TRANSFORMATIONS] Complex data flows\n`); - - // Example 1: Custom filter operator - console.log(`[1] Custom filter with effect-based logic:\n`); - - const filterByEffect = ( - predicate: (a: A) => Effect.Effect - ) => - (stream: Stream.Stream) => - stream.pipe( - Stream.mapEffect((value) => - predicate(value).pipe( - Effect.map((keep) => (keep ? value : null)) - ) - ), - Stream.filter((value) => value !== null) - ); - - const isValid = (num: number): Effect.Effect => - Effect.gen(function* () { - // Simulate validation effect (e.g., API call) - return num > 0 && num < 100; - }); - - const numbers = [50, 150, 25, -10, 75]; - - const validNumbers = yield* Stream.fromIterable(numbers).pipe( - filterByEffect(isValid), - Stream.runCollect - ); - - yield* Effect.log(`[VALID] ${validNumbers.join(", ")}\n`); - - // Example 2: Enrichment transformation - console.log(`[2] Enriching records with additional data:\n`); - - interface RawRecord { - id: string; - value: number; - } - - interface EnrichedRecord { - id: string; - value: number; - validated: boolean; - processed: Date; - metadata: Record; - } - - const enrich = (record: RawRecord): Effect.Effect => - Effect.gen(function* () { - // Simulate lookup/validation - const validated = record.value > 0; - - return { - id: record.id, - value: record.value, - validated, - processed: new Date(), - metadata: { source: "stream" }, - }; - }); - - const rawData = [ - { id: "r1", value: 10 }, - { id: "r2", value: -5 }, - { id: "r3", value: 20 }, - ]; - - const enriched = yield* Stream.fromIterable(rawData).pipe( - Stream.mapEffect((record) => enrich(record)), - Stream.runCollect - ); - - yield* Effect.log(`[ENRICHED] ${enriched.length} records enriched\n`); - - // Example 3: Demultiplexing (split one stream into multiple) - console.log(`[3] Demultiplexing by category:\n`); - - interface Event { - id: string; - type: "click" | "view" | "purchase"; - data: unknown; - } - - const events: Event[] = [ - { id: "e1", type: "click", data: { x: 100, y: 200 } }, - { id: "e2", type: "view", data: { url: "/" } }, - { id: "e3", type: "purchase", data: { amount: 99.99 } }, - { id: "e4", type: "click", data: { x: 50, y: 100 } }, - ]; - - const clicks = yield* Stream.fromIterable(events).pipe( - Stream.filter((e) => e.type === "click"), - Stream.runCollect - ); - - const views = yield* Stream.fromIterable(events).pipe( - Stream.filter((e) => e.type === "view"), - Stream.runCollect - ); - - const purchases = yield* Stream.fromIterable(events).pipe( - Stream.filter((e) => e.type === "purchase"), - Stream.runCollect - ); - - yield* Effect.log( - `[DEMUX] Clicks: ${clicks.length}, Views: ${views.length}, Purchases: ${purchases.length}\n` - ); - - // Example 4: Chunked processing (batch transformation) - console.log(`[4] Chunked processing (batches of N):\n`); - - const processChunk = (chunk: Array<{ id: string; value: number }>) => - Effect.gen(function* () { - const sum = chunk.reduce((s, r) => s + r.value, 0); - const avg = sum / chunk.length; - - yield* Effect.log( - `[CHUNK] ${chunk.length} items, avg: ${avg.toFixed(2)}` - ); - - return { size: chunk.length, sum, avg }; - }); - - const data = Array.from({ length: 10 }, (_, i) => ({ - id: `d${i}`, - value: i + 1, - })); - - const chunkSize = 3; - const chunks = []; - - for (let i = 0; i < data.length; i += chunkSize) { - const chunk = data.slice(i, i + chunkSize); - - chunks.push(chunk); - } - - const chunkResults = yield* Effect.all( - chunks.map((chunk) => processChunk(chunk)) - ); - - yield* Effect.log( - `[CHUNKS] Processed ${chunkResults.length} batches\n` - ); - - // Example 5: Multi-stage transformation pipeline - console.log(`[5] Multi-stage pipeline (parse → validate → transform):\n`); - - const rawStrings = ["10", "twenty", "30", "-5", "50"]; - - // Stage 1: Parse - const parsed = yield* Stream.fromIterable(rawStrings).pipe( - Stream.mapEffect((s) => - Effect.gen(function* () { - try { - return parseInt(s); - } catch (error) { - yield* Effect.fail( - new Error(`Failed to parse: ${s}`) - ); - } - }).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.log(`[PARSE ERROR] ${error.message}`); - - return null; - }) - ) - ) - ), - Stream.filter((n) => n !== null), - Stream.runCollect - ); - - yield* Effect.log(`[STAGE 1] Parsed: ${parsed.join(", ")}`); - - // Stage 2: Validate - const validated = parsed.filter((n) => n > 0); - - yield* Effect.log(`[STAGE 2] Validated: ${validated.join(", ")}`); - - // Stage 3: Transform - const transformed = validated.map((n) => n * 2); - - yield* Effect.log(`[STAGE 3] Transformed: ${transformed.join(", ")}\n`); - - // Example 6: Composition of custom operators - console.log(`[6] Composable transformation pipeline:\n`); - - // Define custom operator - const withLogging = (label: string) => - (stream: Stream.Stream) => - stream.pipe( - Stream.tap((value) => - Effect.log(`[${label}] Processing: ${JSON.stringify(value)}`) - ) - ); - - const filterPositive = (stream: Stream.Stream) => - stream.pipe( - Stream.filter((n) => n > 0), - Stream.tap(() => Effect.log(`[FILTER] Kept positive`)) - ); - - const scaleUp = (factor: number) => - (stream: Stream.Stream) => - stream.pipe( - Stream.map((n) => n * factor), - Stream.tap((n) => - Effect.log(`[SCALE] Scaled to ${n}`) - ) - ); - - const testData = [10, -5, 20, -3, 30]; - - const pipeline = yield* Stream.fromIterable(testData).pipe( - withLogging("INPUT"), - filterPositive, - scaleUp(10), - Stream.runCollect - ); - - yield* Effect.log(`[RESULT] Final: ${pipeline.join(", ")}\n`); - - // Example 7: Stateful transformation - console.log(`[7] Stateful transformation (running total):\n`); - - const runningTotal = yield* Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.scan(0, (acc, value) => acc + value), - Stream.runCollect - ); - - yield* Effect.log(`[TOTALS] ${runningTotal.join(", ")}\n`); - - // Example 8: Conditional transformation - console.log(`[8] Conditional transformation (different paths):\n`); - - interface Item { - id: string; - priority: "high" | "normal" | "low"; - } - - const transformByPriority = (item: Item): Effect.Effect<{ - id: string; - processed: string; - }> => - Effect.gen(function* () { - switch (item.priority) { - case "high": - yield* Effect.log(`[HIGH] Priority processing for ${item.id}`); - - return { id: item.id, processed: "urgent" }; - - case "normal": - yield* Effect.log( - `[NORMAL] Standard processing for ${item.id}` - ); - - return { id: item.id, processed: "standard" }; - - case "low": - yield* Effect.log(`[LOW] Deferred processing for ${item.id}`); - - return { id: item.id, processed: "deferred" }; - } - }); - - const items: Item[] = [ - { id: "i1", priority: "normal" }, - { id: "i2", priority: "high" }, - { id: "i3", priority: "low" }, - ]; - - const processed = yield* Stream.fromIterable(items).pipe( - Stream.mapEffect((item) => transformByPriority(item)), - Stream.runCollect - ); - - yield* Effect.log( - `[CONDITIONAL] Processed ${processed.length} items\n` - ); - - // Example 9: Performance-optimized transformation - console.log(`[9] Optimized for performance:\n`); - - const largeDataset = Array.from({ length: 1000 }, (_, i) => i); - - const startTime = Date.now(); - - // Use efficient operators - const result = yield* Stream.fromIterable(largeDataset).pipe( - Stream.filter((n) => n % 2 === 0), // Keep even - Stream.take(100), // Limit to first 100 - Stream.map((n) => n * 2), // Transform - Stream.runCollect - ); - - const elapsed = Date.now() - startTime; - - yield* Effect.log( - `[PERF] Processed 1000 items in ${elapsed}ms, kept ${result.length} items` - ); -}); - -Effect.runPromise(program); -``` - ---- - -### Explanation - -Simple transformations don't scale: - -**Problem 1: Performance degradation** -- Each layer creates intermediate collection -- 10 transformations = 10 allocations -- Process 1M items = 10M allocations -- GC pressure, memory exhaustion - -**Problem 2: Complex logic scattered** -- Validation here, enrichment there, filtering elsewhere -- Hard to maintain -- Changes break other parts -- No clear data flow - -**Problem 3: Effect handling** -- Transformations need side effects -- Network calls, database queries -- Naive approach: load all, transform sequentially -- Slow, inefficient - -**Problem 4: Reusability** -- Custom transformation used once -- Next time, rewrite from scratch -- Code duplication -- Bugs replicated - -Solutions: - -**Custom operators**: -- Encapsulate transformation logic -- Reusable across projects -- Testable in isolation -- Composable - -**Lazy evaluation**: -- Compute as elements flow -- No intermediate collections -- Constant memory -- Only compute what's used - -**Fusion**: -- Combine multiple maps/filters -- Single pass through data -- No intermediate collections -- Compiler/library optimizes - -**Effect composition**: -- Chain effects naturally -- Error propagation automatic -- Resource cleanup guaranteed -- Readable code - ---- - ---- - -## Stream vs Effect - When to Use Which - -**Rule:** Use Effect for single values, Stream for sequences of values. - -**Skill Level:** beginner - -**Use Cases:** streams-getting-started - -### Good Example - -```typescript -import { Effect, Stream } from "effect" - -// ============================================ -// EFFECT: Single result operations -// ============================================ - -// Fetch one user - returns Effect -const fetchUser = (id: string) => - Effect.tryPromise(() => - fetch(`/api/users/${id}`).then((r) => r.json()) - ) - -// Read entire config - returns Effect -const loadConfig = Effect.tryPromise(() => - fetch("/config.json").then((r) => r.json()) -) - -// ============================================ -// STREAM: Multiple values operations -// ============================================ - -// Process file line by line - returns Stream -const fileLines = Stream.fromIterable([ - "line 1", - "line 2", - "line 3", -]) - -// Generate events over time - returns Stream -const events = Stream.make( - { type: "click", x: 10 }, - { type: "click", x: 20 }, - { type: "scroll", y: 100 }, -) - -// ============================================ -// CONVERTING BETWEEN THEM -// ============================================ - -// Effect → Stream (single value becomes 1-element stream) -const effectToStream = Stream.fromEffect(fetchUser("123")) - -// Stream → Effect (collect all values into array) -const streamToEffect = Stream.runCollect(fileLines) - -// Stream → Effect (process each value for side effects) -const processAll = fileLines.pipe( - Stream.runForEach((line) => Effect.log(`Processing: ${line}`)) -) - -// ============================================ -// DECISION GUIDE -// ============================================ - -// Use Effect when: -// - Fetching a single resource -// - Computing a single result -// - Performing one action - -// Use Stream when: -// - Reading files line by line -// - Processing paginated API results -// - Handling real-time events -// - Processing large datasets -// - Building data pipelines -``` - -### Explanation - -Both Effect and Stream are lazy and composable, but they serve different purposes: - -| Aspect | Effect | Stream | -|--------|--------|--------| -| **Produces** | One value | Zero or more values | -| **Memory** | Holds one result | Processes incrementally | -| **Use case** | API call, DB query | File lines, events, batches | - ---- - ---- - -## 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. - -**Skill Level:** intermediate - -**Use Cases:** tooling-and-debugging - -### Good 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.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. - ---- - -### Anti-Pattern - -Going without the LSP. While your code will still compile and work perfectly fine, you are essentially "flying blind." You miss out on the rich, real-time feedback that the LSP provides, forcing you to rely more heavily on manual type checking, `tsc` runs, and deciphering complex inferred types from your editor's default tooltips. This leads to a slower, less efficient development cycle. - -### Explanation - -Effect's type system is incredibly powerful, but TypeScript's default language server doesn't always display the rich information contained within the `A`, `E`, and `R` channels in the most intuitive way. - -The Effect LSP is a specialized tool that understands the semantics of Effect. It hooks into your editor to provide a superior experience: - -- **Rich Inline Types:** It displays the full `Effect` signature directly in your code as you work, so you always know exactly what an effect produces, how it can fail, and what it requires. -- **Clear Error Messages:** It provides more specific and helpful error messages tailored to Effect's APIs. -- **Enhanced Autocompletion:** It can offer more context-aware suggestions. - -This tool essentially makes the compiler's knowledge visible at a glance, reducing the mental overhead of tracking complex types and allowing you to catch errors before you even save the file. - ---- - ---- - -## Take and Drop Stream Elements - -**Rule:** Use take/drop to control stream size, takeWhile/dropWhile for conditional limits. - -**Skill Level:** beginner - -**Use Cases:** streams-getting-started - -### Good Example - -```typescript -import { Effect, Stream } from "effect" - -const numbers = Stream.make(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) - -// ============================================ -// take - Get first N elements -// ============================================ - -const firstThree = numbers.pipe( - Stream.take(3), - Stream.runCollect -) - -Effect.runPromise(firstThree).then((chunk) => { - console.log([...chunk]) // [1, 2, 3] -}) - -// ============================================ -// drop - Skip first N elements -// ============================================ - -const skipThree = numbers.pipe( - Stream.drop(3), - Stream.runCollect -) - -Effect.runPromise(skipThree).then((chunk) => { - console.log([...chunk]) // [4, 5, 6, 7, 8, 9, 10] -}) - -// ============================================ -// Combine for pagination (skip + limit) -// ============================================ - -const page2 = numbers.pipe( - Stream.drop(3), // Skip first page - Stream.take(3), // Take second page - Stream.runCollect -) - -Effect.runPromise(page2).then((chunk) => { - console.log([...chunk]) // [4, 5, 6] -}) - -// ============================================ -// takeWhile - Take while condition is true -// ============================================ - -const untilFive = numbers.pipe( - Stream.takeWhile((n) => n < 5), - Stream.runCollect -) - -Effect.runPromise(untilFive).then((chunk) => { - console.log([...chunk]) // [1, 2, 3, 4] -}) - -// ============================================ -// dropWhile - Skip while condition is true -// ============================================ - -const afterFive = numbers.pipe( - Stream.dropWhile((n) => n < 5), - Stream.runCollect -) - -Effect.runPromise(afterFive).then((chunk) => { - console.log([...chunk]) // [5, 6, 7, 8, 9, 10] -}) - -// ============================================ -// takeUntil - Take until condition becomes true -// ============================================ - -const untilSix = numbers.pipe( - Stream.takeUntil((n) => n === 6), - Stream.runCollect -) - -Effect.runPromise(untilSix).then((chunk) => { - console.log([...chunk]) // [1, 2, 3, 4, 5, 6] -}) - -// ============================================ -// Practical: Process file with header -// ============================================ - -const fileLines = Stream.make( - "# Header", - "# Comment", - "data1", - "data2", - "data3" -) - -const dataOnly = fileLines.pipe( - Stream.dropWhile((line) => line.startsWith("#")), - Stream.runCollect -) - -Effect.runPromise(dataOnly).then((chunk) => { - console.log([...chunk]) // ["data1", "data2", "data3"] -}) -``` - -### Explanation - -Streams can be infinite or very large. These operators let you: - -1. **Limit processing** - Only take what you need -2. **Skip headers** - Drop first N elements -3. **Conditional limits** - Take/drop based on predicates -4. **Pagination** - Implement skip/limit patterns - ---- - ---- - -## Teach your AI Agents Effect with the MCP Server - -**Rule:** Use the MCP server to provide live application context to AI coding agents, enabling more accurate assistance. - -**Skill Level:** advanced - -**Use Cases:** tooling-and-debugging - -### Good Example - -The "Good Example" is the workflow this pattern enables. - -1. **You run the MCP server** in your terminal, pointing it at your main `AppLayer`. - - ```bash - npx @effect/mcp-server --layer src/layers.ts:AppLayer - ``` - -2. **You configure your AI agent** (e.g., Cursor) to use the MCP server's endpoint (`http://localhost:3333`). - -3. **You ask the AI a question** that requires deep context about your app: - - > "Refactor this code to use the `UserService` to fetch a user by ID and log the result with the `Logger`." - -4. **The AI, in the background, queries the MCP server:** - - - It discovers that `UserService` and `Logger` are available in the `AppLayer`. - - It retrieves the exact method signature for `UserService.getUser` and `Logger.log`. - -5. **The AI generates correct, context-aware code** because it's not guessing; it's using the live architectural information provided by the MCP server. - -```typescript -// The AI generates this correct code: -import { Effect } from "effect"; -import { UserService } from "./features/User/UserService.js"; -const program = Effect.gen(function* () { - const userService = yield* UserService; - - const user = yield* userService.getUser("123"); - yield* Effect.log(`Found user: ${user.name}`); -}); -``` - ---- - -### Anti-Pattern - -Working with an AI agent without providing it with specific context. The agent will be forced to guess based on open files or generic knowledge. This often leads to it hallucinating method names, getting dependency injection wrong, or failing to handle specific error types, requiring you to manually correct its output and defeating the purpose of using an AI assistant. - -### Explanation - -AI coding agents are powerful, but they often lack the deep, structural understanding of a complex Effect application. They might not know which services are available in the context, what a specific `Layer` provides, or how your feature modules are composed. - -The MCP server solves this problem. It's a specialized server that runs alongside your application during development. It inspects your `AppLayer` and creates a real-time, queryable model of your entire application architecture. - -An AI agent can then connect to this MCP server to ask specific questions before generating code, such as: - -- "What services are available in the current context?" -- "What is the full API of the `UserService`?" -- "What errors can `UserRepository.findById` fail with?" - -By providing this live, ground-truth context, you transform your AI from a generic coding assistant into a specialized expert on _your_ specific codebase, resulting in far more accurate and useful code generation and refactoring. - ---- - ---- - -## Test Concurrent Code - -**Rule:** Use TestClock and controlled concurrency to make concurrent tests deterministic. - -**Skill Level:** advanced - -**Use Cases:** testing - -### Good Example - -```typescript -import { describe, it, expect } from "vitest" -import { Effect, Fiber, Ref, TestClock, Duration, Deferred } from "effect" - -describe("Concurrent Code Testing", () => { - // ============================================ - // 1. Test parallel execution - // ============================================ - - it("should run effects in parallel", async () => { - const executionOrder: string[] = [] - - const task1 = Effect.gen(function* () { - yield* Effect.sleep("100 millis") - executionOrder.push("task1") - return 1 - }) - - const task2 = Effect.gen(function* () { - yield* Effect.sleep("50 millis") - executionOrder.push("task2") - return 2 - }) - - const program = Effect.all([task1, task2], { concurrency: 2 }) - - // Use TestClock to control time - const result = await Effect.runPromise( - Effect.gen(function* () { - const fiber = yield* Effect.fork(program) - - // Advance time to trigger both tasks - yield* TestClock.adjust("100 millis") - - return yield* Fiber.join(fiber) - }).pipe(Effect.provide(TestClock.live)) - ) - - expect(result).toEqual([1, 2]) - // With real time, task2 would complete first - expect(executionOrder).toContain("task1") - expect(executionOrder).toContain("task2") - }) - - // ============================================ - // 2. Test race conditions - // ============================================ - - it("should handle race condition correctly", async () => { - const counter = await Effect.runPromise( - Effect.gen(function* () { - const ref = yield* Ref.make(0) - - // Simulate concurrent increments - const increment = Ref.update(ref, (n) => n + 1) - - // Run 100 concurrent increments - yield* Effect.all( - Array.from({ length: 100 }, () => increment), - { concurrency: "unbounded" } - ) - - return yield* Ref.get(ref) - }) - ) - - // Ref is atomic, so all increments should be counted - expect(counter).toBe(100) - }) - - // ============================================ - // 3. Test with controlled fiber execution - // ============================================ - - it("should test fiber lifecycle", async () => { - const events: string[] = [] - - const program = Effect.gen(function* () { - const fiber = yield* Effect.fork( - Effect.gen(function* () { - events.push("started") - yield* Effect.sleep("1 second") - events.push("completed") - return "result" - }) - ) - - events.push("forked") - - // Interrupt the fiber - yield* Fiber.interrupt(fiber) - events.push("interrupted") - - const exit = yield* Fiber.await(fiber) - return exit - }) - - await Effect.runPromise(program) - - expect(events).toEqual(["forked", "started", "interrupted"]) - expect(events).not.toContain("completed") - }) - - // ============================================ - // 4. Test timeout behavior - // ============================================ - - it("should timeout slow operations", async () => { - const slowOperation = Effect.gen(function* () { - yield* Effect.sleep("10 seconds") - return "completed" - }) - - const result = await Effect.runPromise( - Effect.gen(function* () { - const fiber = yield* Effect.fork( - slowOperation.pipe(Effect.timeout("1 second")) - ) - - // Advance past the timeout - yield* TestClock.adjust("2 seconds") - - return yield* Fiber.join(fiber) - }).pipe(Effect.provide(TestClock.live)) - ) - - // Result is Option.None due to timeout - expect(result._tag).toBe("None") - }) - - // ============================================ - // 5. Test with Deferred for synchronization - // ============================================ - - it("should synchronize fibers correctly", async () => { - const result = await Effect.runPromise( - Effect.gen(function* () { - const deferred = yield* Deferred.make() - const results: string[] = [] - - // Consumer waits for producer - const consumer = Effect.fork( - Effect.gen(function* () { - const value = yield* Deferred.await(deferred) - results.push(`consumed: ${value}`) - }) - ) - - // Producer completes the deferred - const producer = Effect.gen(function* () { - results.push("producing") - yield* Deferred.succeed(deferred, "data") - results.push("produced") - }) - - yield* consumer - yield* producer - - // Wait for consumer to process - yield* Effect.sleep("10 millis") - - return results - }) - ) - - expect(result).toContain("producing") - expect(result).toContain("produced") - expect(result).toContain("consumed: data") - }) - - // ============================================ - // 6. Test for absence of deadlocks - // ============================================ - - it("should not deadlock with proper resource ordering", async () => { - const result = await Effect.runPromise( - Effect.gen(function* () { - const ref1 = yield* Ref.make(0) - const ref2 = yield* Ref.make(0) - - // Two fibers accessing refs in same order (no deadlock) - const fiber1 = yield* Effect.fork( - Effect.gen(function* () { - yield* Ref.update(ref1, (n) => n + 1) - yield* Ref.update(ref2, (n) => n + 1) - }) - ) - - const fiber2 = yield* Effect.fork( - Effect.gen(function* () { - yield* Ref.update(ref1, (n) => n + 1) - yield* Ref.update(ref2, (n) => n + 1) - }) - ) - - yield* Fiber.join(fiber1) - yield* Fiber.join(fiber2) - - return [yield* Ref.get(ref1), yield* Ref.get(ref2)] - }).pipe(Effect.timeout("1 second")) - ) - - expect(result._tag).toBe("Some") - expect(result.value).toEqual([2, 2]) - }) -}) -``` - -### Explanation - -Concurrent code is hard to test: - -1. **Non-determinism** - Different runs, different results -2. **Race conditions** - Timing-dependent bugs -3. **Deadlocks** - Hard to reproduce -4. **Flaky tests** - Pass sometimes, fail others - -Effect's test utilities provide control over timing and concurrency. - ---- - ---- - -## Test Effects with Services - -**Rule:** Provide test implementations of services to make Effect programs testable. - -**Skill Level:** beginner - -**Use Cases:** testing - -### Good Example - -```typescript -import { describe, it, expect } from "vitest" -import { Effect, Context } from "effect" - -// ============================================ -// 1. Define a service -// ============================================ - -class UserRepository extends Context.Tag("UserRepository")< - UserRepository, - { - readonly findById: (id: string) => Effect.Effect - readonly save: (user: User) => Effect.Effect - } ->() {} - -interface User { - id: string - name: string - email: string -} - -// ============================================ -// 2. Code that uses the service -// ============================================ - -const getUser = (id: string) => - Effect.gen(function* () { - const repo = yield* UserRepository - const user = yield* repo.findById(id) - - if (!user) { - return yield* Effect.fail(new Error(`User ${id} not found`)) - } - - return user - }) - -const createUser = (name: string, email: string) => - Effect.gen(function* () { - const repo = yield* UserRepository - - const user: User = { - id: crypto.randomUUID(), - name, - email, - } - - yield* repo.save(user) - return user - }) - -// ============================================ -// 3. Create a test implementation -// ============================================ - -const makeTestUserRepository = (initialUsers: User[] = []) => { - const users = new Map(initialUsers.map(u => [u.id, u])) - - return UserRepository.of({ - findById: (id) => Effect.succeed(users.get(id) ?? null), - save: (user) => Effect.sync(() => { users.set(user.id, user) }), - }) -} - -// ============================================ -// 4. Write tests -// ============================================ - -describe("User Service Tests", () => { - it("should find an existing user", async () => { - const testUser: User = { - id: "123", - name: "Alice", - email: "alice@example.com", - } - - const testRepo = makeTestUserRepository([testUser]) - - const result = await Effect.runPromise( - getUser("123").pipe( - Effect.provideService(UserRepository, testRepo) - ) - ) - - expect(result).toEqual(testUser) - }) - - it("should fail when user not found", async () => { - const testRepo = makeTestUserRepository([]) - - await expect( - Effect.runPromise( - getUser("999").pipe( - Effect.provideService(UserRepository, testRepo) - ) - ) - ).rejects.toThrow("User 999 not found") - }) - - it("should create and save a user", async () => { - const savedUsers: User[] = [] - - const trackingRepo = UserRepository.of({ - findById: () => Effect.succeed(null), - save: (user) => Effect.sync(() => { savedUsers.push(user) }), - }) - - const result = await Effect.runPromise( - createUser("Bob", "bob@example.com").pipe( - Effect.provideService(UserRepository, trackingRepo) - ) - ) - - expect(result.name).toBe("Bob") - expect(result.email).toBe("bob@example.com") - expect(savedUsers).toHaveLength(1) - expect(savedUsers[0].name).toBe("Bob") - }) -}) -``` - -### Explanation - -Effect's service pattern makes testing easy: - -1. **Declare dependencies** - Effects specify what they need -2. **Inject test doubles** - Provide fake implementations for tests -3. **No mocking libraries** - Just provide different service implementations -4. **Type-safe** - Compiler ensures you provide all dependencies - ---- - ---- - -## Test Streaming Effects - -**Rule:** Use Stream.runCollect and assertions to verify stream behavior. - -**Skill Level:** advanced - -**Use Cases:** testing - -### Good Example - -```typescript -import { describe, it, expect } from "vitest" -import { Effect, Stream, Chunk, Ref } from "effect" - -describe("Stream Testing", () => { - // ============================================ - // 1. Test basic stream operations - // ============================================ - - it("should transform stream elements", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.map((n) => n * 2), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6, 8, 10]) - }) - - it("should filter stream elements", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5, 6]).pipe( - Stream.filter((n) => n % 2 === 0), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6]) - }) - - // ============================================ - // 2. Test stream aggregation - // ============================================ - - it("should fold stream to single value", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.runFold(0, (acc, n) => acc + n) - ) - ) - - expect(result).toBe(15) - }) - - it("should count stream elements", async () => { - const count = await Effect.runPromise( - Stream.fromIterable(["a", "b", "c", "d"]).pipe( - Stream.runCount - ) - ) - - expect(count).toBe(4) - }) - - // ============================================ - // 3. Test error handling in streams - // ============================================ - - it("should catch errors in stream", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3]).pipe( - Stream.mapEffect((n) => - n === 2 - ? Effect.fail(new Error("Failed on 2")) - : Effect.succeed(n * 10) - ), - Stream.catchAll((error) => - Stream.succeed(-1) // Replace error with sentinel - ), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([10, -1]) - }) - - it("should handle errors and continue with orElse", async () => { - const failingStream = Stream.fail(new Error("Primary failed")) - const fallbackStream = Stream.fromIterable([1, 2, 3]) - - const result = await Effect.runPromise( - failingStream.pipe( - Stream.orElse(() => fallbackStream), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3]) - }) - - // ============================================ - // 4. Test stream chunking - // ============================================ - - it("should chunk stream elements", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.grouped(2), - Stream.runCollect - ) - ) - - const chunks = Chunk.toReadonlyArray(result).map(Chunk.toReadonlyArray) - expect(chunks).toEqual([[1, 2], [3, 4], [5]]) - }) - - // ============================================ - // 5. Test stream with effects - // ============================================ - - it("should run effects for each element", async () => { - const processed: number[] = [] - - await Effect.runPromise( - Stream.fromIterable([1, 2, 3]).pipe( - Stream.tap((n) => - Effect.sync(() => { - processed.push(n) - }) - ), - Stream.runDrain - ) - ) - - expect(processed).toEqual([1, 2, 3]) - }) - - // ============================================ - // 6. Test stream resource management - // ============================================ - - it("should release resources on completion", async () => { - const acquired: string[] = [] - const released: string[] = [] - - const managedStream = Stream.acquireRelease( - Effect.gen(function* () { - acquired.push("resource") - return "resource" - }), - () => - Effect.sync(() => { - released.push("resource") - }) - ).pipe( - Stream.flatMap(() => Stream.fromIterable([1, 2, 3])) - ) - - await Effect.runPromise(Stream.runDrain(managedStream)) - - expect(acquired).toEqual(["resource"]) - expect(released).toEqual(["resource"]) - }) - - it("should release resources on error", async () => { - const released: string[] = [] - - const managedStream = Stream.acquireRelease( - Effect.succeed("resource"), - () => Effect.sync(() => { released.push("released") }) - ).pipe( - Stream.flatMap(() => - Stream.fromEffect(Effect.fail(new Error("Oops"))) - ) - ) - - await Effect.runPromise( - Stream.runDrain(managedStream).pipe( - Effect.catchAll(() => Effect.void) - ) - ) - - expect(released).toEqual(["released"]) - }) - - // ============================================ - // 7. Test stream timing with take/drop - // ============================================ - - it("should take first N elements", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.take(3), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3]) - }) - - it("should drop first N elements", async () => { - const result = await Effect.runPromise( - Stream.fromIterable([1, 2, 3, 4, 5]).pipe( - Stream.drop(2), - Stream.runCollect - ) - ) - - expect(Chunk.toReadonlyArray(result)).toEqual([3, 4, 5]) - }) - - // ============================================ - // 8. Test stream merging - // ============================================ - - it("should merge streams", async () => { - const stream1 = Stream.fromIterable([1, 3, 5]) - const stream2 = Stream.fromIterable([2, 4, 6]) - - const result = await Effect.runPromise( - Stream.merge(stream1, stream2).pipe( - Stream.runCollect - ) - ) - - const array = Chunk.toReadonlyArray(result) - expect(array).toHaveLength(6) - expect(array).toContain(1) - expect(array).toContain(6) - }) -}) -``` - -### Explanation - -Stream tests verify: - -1. **Transformations** - map, filter, flatMap work correctly -2. **Error handling** - Failures are caught and handled -3. **Resource safety** - Resources are released -4. **Backpressure** - Data flow is controlled - ---- - ---- - -## Trace Operations Across Services with Spans - -**Rule:** Use Effect.withSpan to create and annotate tracing spans for operations, enabling distributed tracing and performance analysis. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Trace a database query with a custom span -const fetchUser = Effect.sync(() => { - // ...fetch user from database - return { id: 1, name: "Alice" }; -}).pipe(Effect.withSpan("db.fetchUser")); - -// Trace an HTTP request with additional attributes -const fetchData = Effect.tryPromise({ - try: () => fetch("https://api.example.com/data").then((res) => res.json()), - catch: (err) => `Network error: ${String(err)}`, -}).pipe( - Effect.withSpan("http.fetchData", { - attributes: { url: "https://api.example.com/data" }, - }) -); - -// Use spans in a workflow -const program = Effect.gen(function* () { - yield* Effect.log("Starting workflow").pipe( - Effect.withSpan("workflow.start") - ); - const user = yield* fetchUser; - yield* Effect.log(`Fetched user: ${user.name}`).pipe( - Effect.withSpan("workflow.end") - ); -}); -``` - -**Explanation:** - -- `Effect.withSpan` creates a tracing span around an operation. -- Spans can be named and annotated with attributes for richer context. -- Tracing enables distributed observability and performance analysis. - -### Anti-Pattern - -Relying only on logs or metrics for performance analysis, or lacking visibility into the flow of requests and operations across services. - -### Explanation - -Tracing spans help you understand the flow and timing of operations, especially in distributed systems or complex workflows. -They allow you to pinpoint bottlenecks, visualize dependencies, and correlate logs and metrics with specific requests. - ---- - -## Trace Operations Across Services with Spans - -**Rule:** Use Effect.withSpan to create custom tracing spans for important operations. - -**Skill Level:** intermediate - -**Use Cases:** observability - -### Good 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); -``` - ---- - -### Anti-Pattern - -Not adding custom spans to your business logic. -Without them, your traces will only show high-level information from your framework (e.g., "HTTP POST /users"). -You will have no visibility into the performance of the individual steps _inside_ your request handler, making it very difficult to pinpoint bottlenecks. Your application's logic remains a "black box" in your traces. - -### Explanation - -While logs tell you _what_ happened, traces tell you _why it was slow_. In a complex application, a single user request might trigger calls to multiple services (authentication, database, external APIs). Tracing allows you to visualize this entire chain of events as a single, hierarchical "trace." - -Each piece of work in that trace is a `span`. `Effect.withSpan` allows you to create your own custom spans. This is invaluable for answering questions like: - -- "For this API request, did we spend most of our time in the database or calling the external payment gateway?" -- "Which part of our user creation logic is the bottleneck?" - -Effect's tracing is built on OpenTelemetry, the industry standard, so it integrates seamlessly with tools like Jaeger, Zipkin, and Datadog. - ---- - ---- - -## Transform Data During Validation with Schema - -**Rule:** Use Schema.transform to safely convert data types during the validation and parsing process. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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; -}); - -const programWithLogging = Effect.gen(function* () { - try { - const event = yield* program; - yield* Effect.log(`Event year: ${event.timestamp.getFullYear()}`); - yield* Effect.log(`Full event: ${JSON.stringify(event, null, 2)}`); - return event; - } catch (error) { - yield* Effect.logError(`Failed to parse event: ${error}`); - throw error; - } -}).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Program error: ${error}`); - return null; - }) - ) -); - -Effect.runPromise(programWithLogging); -``` - - -`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 -``` - ---- - -### Anti-Pattern - -Performing validation and transformation in two separate steps. This is more verbose, requires creating intermediate types, and separates the validation logic from the transformation logic. - -```typescript -import { Schema, Effect } from "effect"; - -// ❌ WRONG: Requires an intermediate "Raw" type. -const RawApiEventSchema = Schema.Struct({ - name: Schema.String, - timestamp: Schema.String, -}); - -const rawInput = { name: "User Login", timestamp: "2025-06-22T20:08:42.000Z" }; - -// The logic is now split into two distinct, less cohesive steps. -const program = Schema.decode(RawApiEventSchema)(rawInput).pipe( - Effect.map((rawEvent) => ({ - ...rawEvent, - timestamp: new Date(rawEvent.timestamp), // Manual transformation after parsing. - })) -); -``` - -### Explanation - -Often, the data you receive from external sources (like an API) isn't in the ideal format for your application's domain model. For example, dates are sent as ISO strings, but you want to work with `Date` objects. - -`Schema.transform` integrates this conversion directly into the parsing step. It takes two functions: one to `decode` the input type into the domain type, and one to `encode` it back. This makes your schema the single source of truth for both the shape and the type transformation of your data. - -For transformations that can fail (like creating a branded type), you can use `Schema.transformOrFail`, which allows the decoding step to return an `Either`. - ---- - ---- - -## Transform Effect Values with map and flatMap - -**Rule:** Transform Effect values with map and flatMap. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -const getUser = (id: number): Effect.Effect<{ id: number; name: string }> => - Effect.succeed({ id, name: "Paul" }); - -const getPosts = (userId: number): Effect.Effect<{ title: string }[]> => - Effect.succeed([{ title: "My First Post" }, { title: "Second Post" }]); - -const userPosts = getUser(123).pipe( - Effect.flatMap((user) => getPosts(user.id)) -); - -// Demonstrate transforming Effect values -const program = Effect.gen(function* () { - yield* Effect.log("=== Transform Effect Values Demo ==="); - - // 1. Basic transformation with map - yield* Effect.log("\n1. Transform with map:"); - const userWithUpperName = yield* getUser(123).pipe( - Effect.map((user) => ({ ...user, name: user.name.toUpperCase() })) - ); - yield* Effect.log("Transformed user:", userWithUpperName); - - // 2. Chain effects with flatMap - yield* Effect.log("\n2. Chain effects with flatMap:"); - const posts = yield* userPosts; - yield* Effect.log("User posts:", posts); - - // 3. Transform and combine multiple effects - yield* Effect.log("\n3. Transform and combine multiple effects:"); - const userWithPosts = yield* getUser(456).pipe( - Effect.flatMap((user) => - getPosts(user.id).pipe( - Effect.map((posts) => ({ - user: user.name, - postCount: posts.length, - titles: posts.map((p) => p.title), - })) - ) - ) - ); - yield* Effect.log("User with posts:", userWithPosts); - - // 4. Transform with tap for side effects - yield* Effect.log("\n4. Transform with tap for side effects:"); - const result = yield* getUser(789).pipe( - Effect.tap((user) => Effect.log(`Processing user: ${user.name}`)), - Effect.map((user) => `Hello, ${user.name}!`) - ); - yield* Effect.log("Final result:", result); - - yield* Effect.log("\n✅ All transformations completed successfully!"); -}); - -Effect.runPromise(program); -``` - -**Explanation:** -Use `flatMap` to chain effects that depend on each other, and `map` for -simple value transformations. - -### Anti-Pattern - -Using `map` when you should be using `flatMap`. This results in a nested -`Effect>`, which is usually not what you want. - -### Explanation - -`Effect.map` is like `Array.prototype.map`. `Effect.flatMap` is like -`Promise.prototype.then` and is used when your transformation function itself -returns an `Effect`. - ---- - -## Transform Values with Effect.map - -**Rule:** Transform Effect values with map. - -**Skill Level:** beginner - -**Use Cases:** getting-started - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Start with an Effect that succeeds with a number -const getNumber = Effect.succeed(5); - -// Transform it: multiply by 2 -const doubled = Effect.map(getNumber, (n) => n * 2); - -// Transform again: convert to string -const asString = Effect.map(doubled, (n) => `The result is ${n}`); - -// Run to see the result -const result = Effect.runSync(asString); -console.log(result); // "The result is 10" -``` - -### Explanation - -Just like `Array.map` transforms array elements, `Effect.map` transforms -the success value of an Effect. This lets you build pipelines of -transformations without running anything until the end. - ---- - -## Transforming Values with map - -**Rule:** Use map to apply a pure function to the value inside an Effect, Stream, Option, or Either. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Stream, Option, Either } from "effect"; - -// Effect: Transform the result of an effect -const effect = Effect.succeed(2).pipe(Effect.map((n) => n * 10)); // Effect - -// Option: Transform an optional value -const option = Option.some(2).pipe(Option.map((n) => n * 10)); // Option - -// Either: Transform a value that may be an error -const either = Either.right(2).pipe(Either.map((n) => n * 10)); // Either - -// Stream: Transform every value in a stream -const stream = Stream.fromIterable([1, 2, 3]).pipe(Stream.map((n) => n * 10)); // Stream -``` - -**Explanation:** -No matter which type you use, `map` lets you apply a function to the value inside, without changing the error or context. - -### Anti-Pattern - -Manually extracting the value (e.g., with `.getOrElse`, `.unsafeRunSync`, or similar) just to transform it, then re-wrapping it. -This breaks composability and loses the benefits of type safety and error handling. - -### Explanation - -`map` is the most fundamental combinator in functional programming. -It allows you to focus on _what_ you want to do with a value, not _how_ to extract it. -The same mental model applies across all major Effect types. - ---- - -## Turn a Paginated API into a Single Stream - -**Rule:** Use Stream.paginateEffect to model a paginated data source as a single, continuous stream. - -**Skill Level:** intermediate - -**Use Cases:** building-data-pipelines - -### Good 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 -const programWithLogging = Effect.gen(function* () { - const result = yield* program; - yield* Effect.log(`Program result: ${result}`); - return result; -}); - -Effect.runPromise(programWithLogging); - -/* -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 -*/ -``` - -### Anti-Pattern - -The anti-pattern is to write manual, imperative logic to handle the pagination loop. This code is stateful, harder to read, and not composable. - -```typescript -import { Effect, Chunk, Option } from "effect"; -// ... same mock API setup ... - -const fetchAllUsersManually = (): Effect.Effect, Error> => - Effect.gen(function* () { - // Manual state management for results and current page - let allFetchedUsers: User[] = []; - let currentPage: Option.Option = Option.some(1); - - // Manual loop to fetch pages - while (Option.isSome(currentPage)) { - const [users, nextPage] = yield* fetchUsersPage(currentPage.value); - allFetchedUsers = allFetchedUsers.concat(Chunk.toArray(users)); - currentPage = nextPage; - } - - return Chunk.fromIterable(allFetchedUsers); - }); - -const program = fetchAllUsersManually().pipe( - Effect.map((users) => users.length) -); - -Effect.runPromise(program).then((totalUsers) => { - console.log(`Total users fetched from all pages: ${totalUsers}`); -}); -``` - -This manual approach is inferior because it forces you to manage state explicitly (`allFetchedUsers`, `currentPage`). The logic is contained within a single, monolithic effect that is not lazy and cannot be easily composed with other stream operators without first collecting all results. `Stream.paginateEffect` abstracts away this entire block of boilerplate code. - -### Explanation - -Calling paginated APIs is a classic programming challenge. It often involves writing complex, stateful, and imperative code with manual loops to fetch one page, check if there's a next page, fetch that page, and so on, all while accumulating the results. This logic is tedious to write and easy to get wrong. - -`Stream.paginateEffect` elegantly solves this by declaratively modeling the pagination process: - -1. **Declarative and Stateless**: You provide a function that knows how to fetch a single page, and the `Stream` handles the looping, state management (the current page token/number), and termination logic for you. Your business logic remains clean and stateless. -2. **Lazy and Efficient**: The stream fetches pages on demand as they are consumed. If a downstream consumer only needs the first 20 items, the stream will only make enough API calls to satisfy that need, rather than wastefully fetching all pages upfront. -3. **Fully Composable**: The result is a standard `Stream`. This means you can pipe the continuous flow of items directly into other powerful operators like `mapEffect` for concurrent processing or `grouped` for batching, without ever thinking about page boundaries again. - ---- - ---- - -## Type Classes for Equality, Ordering, and Hashing with Data.Class - -**Rule:** Use Data.Class to define and derive type classes for your data types, supporting composable equality, ordering, and hashing. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Data, Equal, HashSet } from "effect"; - -// Define custom data types with structural equality -const user1 = Data.struct({ id: 1, name: "Alice" }); -const user2 = Data.struct({ id: 1, name: "Alice" }); -const user3 = Data.struct({ id: 2, name: "Bob" }); - -// Data.struct provides automatic structural equality -console.log(Equal.equals(user1, user2)); // true (same structure) -console.log(Equal.equals(user1, user3)); // false (different values) - -// Use in a HashSet (works because Data.struct implements Equal) -const set = HashSet.make(user1); -console.log(HashSet.has(set, user2)); // true (structural equality) - -// Create an array and use structural equality -const users = [user1, user3]; -console.log(users.some((u) => Equal.equals(u, user2))); // true -``` - -**Explanation:** - -- `Data.Class.getEqual` derives an equality type class for your data type. -- `Data.Class.getOrder` derives an ordering type class, useful for sorting. -- `Data.Class.getHash` derives a hash function for use in sets and maps. -- These type classes make your types fully compatible with Effect’s collections and algorithms. - -### Anti-Pattern - -Relying on reference equality, ad-hoc comparison functions, or not providing type class instances for your custom types, which can lead to bugs and inconsistent behavior in collections. - -### Explanation - -Type classes like `Equal`, `Order`, and `Hash` provide a principled way to define how your types are compared, ordered, and hashed. -This is essential for using your types in sets, maps, and for sorting or deduplication. - ---- - -## Understand Fibers as Lightweight Threads - -**Rule:** Understand that a Fiber is a lightweight, virtual thread managed by the Effect runtime for massive concurrency. - -**Skill Level:** advanced - -**Use Cases:** concurrency - -### Good Example - -This program demonstrates the efficiency of fibers by forking 100,000 of them. Each fiber does a small amount of work (sleeping for 1 second). Trying to do this with 100,000 OS threads would instantly crash any system. - -```typescript -import { Effect, Fiber } from "effect"; - -const program = Effect.gen(function* () { - // Demonstrate the lightweight nature of fibers by creating 100,000 of them - // This would be impossible with OS threads due to memory and context switching overhead - const fiberCount = 100_000; - yield* Effect.log(`Forking ${fiberCount} fibers...`); - - // Create an array of 100,000 simple effects - // Each effect sleeps for 1 second and then returns its index - // This simulates lightweight concurrent tasks - const tasks = Array.from({ length: fiberCount }, (_, i) => - Effect.sleep("1 second").pipe(Effect.as(i)) - ); - - // Fork all of them into background fibers - // Effect.fork creates a new fiber for each task without blocking - // This demonstrates fiber creation scalability - 100k fibers created almost instantly - // Each fiber is much lighter than an OS thread (typically ~1KB vs ~8MB per thread) - const fibers = yield* Effect.forEach(tasks, Effect.fork); - - yield* Effect.log( - "All fibers have been forked. Now waiting for them to complete..." - ); - - // Wait for all fibers to finish their work - // Fiber.joinAll waits for all fibers to complete and collects their results - // This demonstrates fiber coordination - managing thousands of concurrent operations - // The runtime efficiently schedules these fibers using a work-stealing thread pool - const results = yield* Fiber.joinAll(fibers); - - yield* Effect.log(`All ${results.length} fibers have completed.`); - - // Key insights from this example: - // 1. Fibers are extremely lightweight - 100k fibers use minimal memory - // 2. Fiber creation is fast - no expensive OS thread allocation - // 3. The Effect runtime efficiently schedules fibers across available CPU cores - // 4. Fibers can be suspended and resumed without blocking OS threads - // 5. This enables massive concurrency for I/O-bound operations -}); - -// This program runs successfully, demonstrating the low overhead of fibers. -// Try running this with OS threads - you'd likely hit system limits around 1000-10000 threads -// With fibers, 100k+ concurrent operations are easily achievable -Effect.runPromise(program); -``` - ---- - -### Anti-Pattern - -The anti-pattern is thinking that a `Fiber` is the same as an OS thread. This can lead to incorrect assumptions about performance and behavior. - -- **Don't assume parallelism on CPU-bound tasks:** In a standard Node.js environment, all fibers run on a single OS thread. If you run 10 CPU-intensive tasks on 10 fibers, they will not run in parallel on 10 different CPU cores. They will share time on the single main thread. Fibers provide massive concurrency for I/O-bound tasks (like network requests), not CPU-bound parallelism. -- **Don't worry about blocking:** A `Fiber` that is "sleeping" or waiting for I/O (like `Effect.sleep` or a `fetch` request) does not block the underlying OS thread. The Effect runtime simply puts it aside and uses the thread to run other ready fibers. - -### Explanation - -In traditional multi-threaded programming, each thread is managed by the operating system, consumes significant memory (for its stack), and involves expensive context switching. This limits the number of concurrent threads you can realistically create. - -Effect's `Fiber`s are different. They are managed entirely by the Effect runtime, not the OS. They are incredibly lightweight data structures that don't have their own OS thread stack. The Effect runtime uses a cooperative scheduling mechanism to run many fibers on a small pool of OS threads (often just one in Node.js). - -This model, known as M:N threading (M fibers on N OS threads), allows for a massive level of concurrency that is impossible with traditional threads. It's what makes Effect so powerful for building highly concurrent applications like servers, data pipelines, and real-time systems. - -When you use operators like `Effect.fork` or `Effect.all`, you are creating new fibers. - ---- - ---- - -## Understand Layers for Dependency Injection - -**Rule:** Understand that a Layer is a blueprint describing how to construct a service and its dependencies. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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.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)); -``` - ---- - -### Anti-Pattern - -Manually creating and passing service instances around. This is the "poor man's DI" and leads to tightly coupled code that is difficult to test and maintain. - -```typescript -// ❌ WRONG: Manual instantiation and prop-drilling. -class LoggerImpl { - log(msg: string) { - console.log(msg); - } -} - -class NotifierImpl { - constructor(private logger: LoggerImpl) {} - notify(msg: string) { - this.logger.log(msg); - } -} - -// Dependencies must be created and passed in manually. -const logger = new LoggerImpl(); -const notifier = new NotifierImpl(logger); - -// This is not easily testable without creating real instances. -notifier.notify("Hello"); -``` - -### Explanation - -In Effect, you don't create service instances directly. Instead, you define `Layer`s that describe _how_ to create them. This separation of declaration from implementation is the core of Effect's powerful dependency injection (DI) system. - -This approach has several key benefits: - -- **Composability:** You can combine small, focused layers into a complete application layer (`Layer.merge`, `Layer.provide`). -- **Declarative Dependencies:** A layer's type signature explicitly documents its own dependencies, making your application's architecture clear and self-documenting. -- **Testability:** For testing, you can easily swap a "live" layer (e.g., one that connects to a real database) with a "test" layer (one that provides mock data) without changing any of your business logic. - ---- - ---- - -## Understand that Effects are Lazy Blueprints - -**Rule:** Understand that effects are lazy blueprints. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -Effect.runSync(Effect.log("1. Defining the Effect blueprint...")); - -const program = Effect.gen(function* () { - yield* Effect.log("3. The blueprint is now being executed!"); - return 42; -}); - -const demonstrationProgram = Effect.gen(function* () { - yield* Effect.log( - "2. The blueprint has been defined. No work has been done yet." - ); - yield* program; -}); - -Effect.runSync(demonstrationProgram); -``` - -**Explanation:** -Defining an `Effect` does not execute any code inside it. Only when you call -`Effect.runSync(program)` does the computation actually happen. - -### Anti-Pattern - -Assuming an `Effect` behaves like a `Promise`. A `Promise` executes its work -immediately upon creation. Never expect a side effect to occur just from -defining an `Effect`. - -### Explanation - -This laziness is a superpower because it makes your code composable, -predictable, and testable. Unlike a `Promise` which executes immediately, -an `Effect` is just a description of work, like a recipe waiting for a chef. - ---- - -## Understand the Three Effect Channels (A, E, R) - -**Rule:** Understand that an Effect<A, E, R> describes a computation with a success type (A), an error type (E), and a requirements type (R). - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -This function signature is a self-documenting contract. It clearly states that to get a `User`, you must provide a `Database` service, and the operation might fail with a `UserNotFoundError`. - -```typescript -import { Effect, Data } from "effect"; - -// Define the types for our channels -interface User { - readonly name: string; -} // The 'A' type -class UserNotFoundError extends Data.TaggedError("UserNotFoundError") {} // The 'E' type - -// Define the Database service using Effect.Service -export class Database extends Effect.Service()("Database", { - // Provide a default implementation - sync: () => ({ - findUser: (id: number) => - id === 1 - ? Effect.succeed({ name: "Paul" }) - : Effect.fail(new UserNotFoundError()), - }), -}) {} - -// This function's signature shows all three channels -const getUser = ( - id: number -): Effect.Effect => - Effect.gen(function* () { - const db = yield* Database; - return yield* db.findUser(id); - }); - -// The program will use the default implementation -const program = getUser(1); - -// Run the program with the default implementation -const programWithLogging = Effect.gen(function* () { - const result = yield* Effect.provide(program, Database.Default); - yield* Effect.log(`Result: ${JSON.stringify(result)}`); // { name: 'Paul' } - return result; -}); - -Effect.runPromise(programWithLogging); -``` - ---- - -### Anti-Pattern - -Ignoring the type system and using generic types. This throws away all the safety and clarity that Effect provides. - -```typescript -import { Effect } from "effect"; - -// ❌ WRONG: This signature is dishonest and unsafe. -// It hides the dependency on a database and the possibility of failure. -function getUserUnsafely(id: number, db: any): Effect.Effect { - try { - const user = db.findUser(id); - if (!user) { - // This will be an unhandled defect, not a typed error. - throw new Error("User not found"); - } - return Effect.succeed(user); - } catch (e) { - // This is also an untyped failure. - return Effect.fail(e); - } -} -``` - -### Explanation - -This three-channel signature is what makes Effect so expressive and safe. Unlike a `Promise` which can only describe its success type, an `Effect`'s signature tells you everything you need to know about a computation before you run it: - -1. **What it produces (`A`):** The data you get on the "happy path." -2. **How it can fail (`E`):** The specific, known errors you need to handle. This makes error handling type-safe and explicit, unlike throwing generic `Error`s. -3. **What it needs (`R`):** The "ingredients" or dependencies required to run the effect. This is the foundation of Effect's powerful dependency injection system. An `Effect` can only be executed when its `R` channel is `never`, meaning all its dependencies have been provided. - -This turns the TypeScript compiler into a powerful assistant that ensures you've handled all possible outcomes and provided all necessary dependencies. - ---- - ---- - -## Understanding Fibers - -**Rule:** Fibers are lightweight threads managed by Effect, enabling efficient concurrency without OS thread overhead. - -**Skill Level:** beginner - -**Use Cases:** concurrency-getting-started - -### Good Example - -```typescript -import { Effect, Fiber } from "effect" - -// ============================================ -// WHAT IS A FIBER? -// ============================================ - -// A fiber is a running effect. When you run an effect, -// it executes on a fiber. - -const myEffect = Effect.gen(function* () { - yield* Effect.log("Hello from a fiber!") - yield* Effect.sleep("100 millis") - return 42 -}) - -// This runs myEffect on the "main" fiber -Effect.runPromise(myEffect) - -// ============================================ -// FORKING: Create a new fiber -// ============================================ - -const withFork = Effect.gen(function* () { - yield* Effect.log("Main fiber starting") - - // Fork creates a new fiber that runs independently - const fiber = yield* Effect.fork( - Effect.gen(function* () { - yield* Effect.log("Child fiber running") - yield* Effect.sleep("200 millis") - yield* Effect.log("Child fiber done") - return "child result" - }) - ) - - yield* Effect.log("Main fiber continues immediately") - yield* Effect.sleep("100 millis") - yield* Effect.log("Main fiber waiting for child...") - - // Wait for the forked fiber to complete - const result = yield* Fiber.join(fiber) - yield* Effect.log(`Got result: ${result}`) -}) - -Effect.runPromise(withFork) -/* -Output: -Main fiber starting -Child fiber running -Main fiber continues immediately -Main fiber waiting for child... -Child fiber done -Got result: child result -*/ - -// ============================================ -// FIBER OPERATIONS -// ============================================ - -const fiberOps = Effect.gen(function* () { - const fiber = yield* Effect.fork( - Effect.gen(function* () { - yield* Effect.sleep("1 second") - return "done" - }) - ) - - // Check if fiber is done (non-blocking) - const poll = yield* Fiber.poll(fiber) - yield* Effect.log(`Poll result: ${poll}`) // None (still running) - - // Wait for completion - const result = yield* Fiber.join(fiber) - yield* Effect.log(`Join result: ${result}`) - - // Or interrupt if taking too long - // yield* Fiber.interrupt(fiber) -}) -``` - -### Explanation - -Unlike OS threads: - -1. **Lightweight** - Create thousands without performance issues -2. **Cooperative** - Yield control at effect boundaries -3. **Interruptible** - Can be cancelled cleanly -4. **Structured** - Parent fibers manage children - ---- - ---- - -## Use .pipe for Composition - -**Rule:** Use .pipe for composition. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -const program = Effect.succeed(5).pipe( - Effect.map((n) => n * 2), - Effect.map((n) => `The result is ${n}`), - Effect.tap(Effect.log) -); - -// Demonstrate various pipe composition patterns -const demo = Effect.gen(function* () { - yield* Effect.log("=== Using Pipe for Composition Demo ==="); - - // 1. Basic pipe composition - yield* Effect.log("\n1. Basic pipe composition:"); - yield* program; - - // 2. Complex pipe composition with multiple transformations - yield* Effect.log("\n2. Complex pipe composition:"); - const complexResult = yield* Effect.succeed(10).pipe( - Effect.map((n) => n + 5), - Effect.map((n) => n * 2), - Effect.tap((n) => Effect.log(`Intermediate result: ${n}`)), - Effect.map((n) => n.toString()), - Effect.map((s) => `Final: ${s}`) - ); - yield* Effect.log("Complex result: " + complexResult); - - // 3. Pipe with flatMap for chaining effects - yield* Effect.log("\n3. Pipe with flatMap for chaining effects:"); - const chainedResult = yield* Effect.succeed("hello").pipe( - Effect.map((s) => s.toUpperCase()), - Effect.flatMap((s) => Effect.succeed(`${s} WORLD`)), - Effect.flatMap((s) => Effect.succeed(`${s}!`)), - Effect.tap((s) => Effect.log(`Chained: ${s}`)) - ); - yield* Effect.log("Chained result: " + chainedResult); - - // 4. Pipe with error handling - yield* Effect.log("\n4. Pipe with error handling:"); - const errorHandledResult = yield* Effect.succeed(-1).pipe( - Effect.flatMap((n) => - n > 0 ? Effect.succeed(n) : Effect.fail(new Error("Negative number")) - ), - Effect.catchAll((error) => - Effect.succeed("Handled error: " + error.message) - ), - Effect.tap((result) => Effect.log(`Error handled: ${result}`)) - ); - yield* Effect.log("Error handled result: " + errorHandledResult); - - // 5. Pipe with multiple operations - yield* Effect.log("\n5. Pipe with multiple operations:"); - const multiOpResult = yield* Effect.succeed([1, 2, 3, 4, 5]).pipe( - Effect.map((arr) => arr.filter((n) => n % 2 === 0)), - Effect.map((arr) => arr.map((n) => n * 2)), - Effect.map((arr) => arr.reduce((sum, n) => sum + n, 0)), - Effect.tap((sum) => Effect.log(`Sum of even numbers doubled: ${sum}`)) - ); - yield* Effect.log("Multi-operation result: " + multiOpResult); - - yield* Effect.log("\n✅ Pipe composition demonstration completed!"); -}); - -Effect.runPromise(demo); -``` - -**Explanation:** -Using `.pipe()` allows you to compose operations in a top-to-bottom style, -improving readability and maintainability. - -### Anti-Pattern - -Nesting function calls manually. This is hard to read and reorder. -`Effect.tap(Effect.map(Effect.map(Effect.succeed(5), n => n * 2), n => ...))` - -### Explanation - -Piping makes code readable and avoids deeply nested function calls. It allows -you to see the flow of data transformations in a clear, linear fashion. - ---- - -## Use Chunk for High-Performance Collections - -**Rule:** Use Chunk to model immutable, high-performance collections for efficient data processing and transformation. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Chunk } from "effect"; - -// Create a Chunk from an array -const numbers = Chunk.fromIterable([1, 2, 3, 4]); // Chunk - -// Map and filter over a Chunk -const doubled = Chunk.map(numbers, (n) => n * 2); // Chunk -const evens = Chunk.filter(numbers, (n) => n % 2 === 0); // Chunk - -// Concatenate Chunks -const moreNumbers = Chunk.fromIterable([5, 6]); -const allNumbers = Chunk.appendAll(numbers, moreNumbers); // Chunk - -// Convert back to array -const arr = Chunk.toArray(allNumbers); // number[] -``` - -**Explanation:** - -- `Chunk` is immutable and optimized for performance. -- It supports efficient batch operations, concatenation, and transformation. -- Use `Chunk` in data pipelines, streaming, and concurrent scenarios. - -### Anti-Pattern - -Using mutable JavaScript arrays for shared or concurrent data, or for large-scale data processing, which can lead to bugs, inefficiency, and unpredictable behavior. - -### Explanation - -`Chunk` provides efficient, immutable operations for large or frequently transformed collections. -It avoids the pitfalls of mutable arrays and is designed for use in concurrent and streaming workflows. - ---- - -## Use Chunk for High-Performance Collections - -**Rule:** Prefer Chunk over Array for immutable collection operations within data processing pipelines for better performance. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good 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, Effect } 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); - -Effect.runSync(Effect.log(finalArray)); // [0, 1, 2] -``` - ---- - -### Anti-Pattern - -Eagerly converting a large or potentially infinite iterable to a `Chunk` before streaming. This completely negates the memory-safety benefits of using a `Stream`. - -```typescript -import { Effect, Stream, Chunk } from "effect"; - -// A generator that could produce a very large (or infinite) number of items. -function* largeDataSource() { - let i = 0; - while (i < 1_000_000) { - yield i++; - } -} - -// ❌ DANGEROUS: `Chunk.fromIterable` will try to pull all 1,000,000 items -// from the generator and load them into memory at once before the stream -// even starts. This can lead to high memory usage or a crash. -const programWithChunk = Stream.fromChunk( - Chunk.fromIterable(largeDataSource()) -).pipe( - Stream.map((n) => n * 2), - Stream.runDrain -); - -// ✅ CORRECT: `Stream.fromIterable` pulls items from the data source lazily, -// one at a time (or in small batches), maintaining constant memory usage. -const programWithIterable = Stream.fromIterable(largeDataSource()).pipe( - Stream.map((n) => n * 2), - Stream.runDrain -); -``` - -### Explanation - -JavaScript's `Array` is a mutable data structure. Every time you perform an "immutable" operation like `[...arr, newItem]` or `arr.map(...)`, you are creating a brand new array and copying all the elements from the old one. For small arrays, this is fine. For large arrays or in hot code paths, this constant allocation and copying can become a performance bottleneck. - -`Chunk` is designed to solve this. It's an immutable data structure that uses structural sharing internally. When you append an item to a `Chunk`, it doesn't re-copy the entire collection. Instead, it creates a new `Chunk` that reuses most of the internal structure of the original, only allocating memory for the new data. This makes immutable appends and updates significantly faster. - ---- - ---- - -## Use Effect DevTools - -**Rule:** Use Effect's built-in debugging features and logging for development. - -**Skill Level:** intermediate - -**Use Cases:** tooling-and-debugging - -### Good Example - -### 1. Enable Debug Mode - -```typescript -import { Effect, Logger, LogLevel, FiberRef, Cause } from "effect" - -// ============================================ -// 1. Verbose logging for development -// ============================================ - -const debugProgram = Effect.gen(function* () { - yield* Effect.logDebug("Starting operation") - - const result = yield* someEffect.pipe( - Effect.tap((value) => Effect.logDebug(`Got value: ${value}`)) - ) - - yield* Effect.logDebug("Operation complete") - return result -}) - -// Run with debug logging enabled -const runWithDebug = debugProgram.pipe( - Logger.withMinimumLogLevel(LogLevel.Debug), - Effect.runPromise -) - -// ============================================ -// 2. Fiber supervision and introspection -// ============================================ - -const inspectFibers = Effect.gen(function* () { - // Fork some fibers - const fiber1 = yield* Effect.fork(Effect.sleep("1 second")) - const fiber2 = yield* Effect.fork(Effect.sleep("2 seconds")) - - // Get fiber IDs - yield* Effect.log(`Fiber 1 ID: ${fiber1.id()}`) - yield* Effect.log(`Fiber 2 ID: ${fiber2.id()}`) - - // Check fiber status - const status1 = yield* fiber1.status - yield* Effect.log(`Fiber 1 status: ${status1._tag}`) -}) - -// ============================================ -// 3. Trace execution with spans -// ============================================ - -const tracedProgram = Effect.gen(function* () { - yield* Effect.log("=== Starting traced program ===") - - yield* Effect.gen(function* () { - yield* Effect.log("Step 1: Initialize") - yield* Effect.sleep("100 millis") - }).pipe(Effect.withLogSpan("initialization")) - - yield* Effect.gen(function* () { - yield* Effect.log("Step 2: Process") - yield* Effect.sleep("200 millis") - }).pipe(Effect.withLogSpan("processing")) - - yield* Effect.gen(function* () { - yield* Effect.log("Step 3: Finalize") - yield* Effect.sleep("50 millis") - }).pipe(Effect.withLogSpan("finalization")) - - yield* Effect.log("=== Program complete ===") -}) - -// ============================================ -// 4. Error cause inspection -// ============================================ - -const debugErrors = Effect.gen(function* () { - const failingEffect = Effect.gen(function* () { - yield* Effect.fail(new Error("Inner error")) - }).pipe( - Effect.flatMap(() => Effect.fail(new Error("Outer error"))) - ) - - yield* failingEffect.pipe( - Effect.catchAllCause((cause) => - Effect.gen(function* () { - yield* Effect.log("=== Error Cause Analysis ===") - yield* Effect.log(`Pretty printed:\n${Cause.pretty(cause)}`) - yield* Effect.log(`Is failure: ${Cause.isFailure(cause)}`) - yield* Effect.log(`Is interrupted: ${Cause.isInterrupted(cause)}`) - - // Extract all failures - const failures = Cause.failures(cause) - yield* Effect.log(`Failures: ${JSON.stringify([...failures])}`) - - return "recovered" - }) - ) - ) -}) - -// ============================================ -// 5. Context inspection -// ============================================ - -import { Context } from "effect" - -class Config extends Context.Tag("Config")() {} - -const inspectContext = Effect.gen(function* () { - const context = yield* Effect.context() - - yield* Effect.log("=== Context Contents ===") - yield* Effect.log(`Has Config: ${Context.getOption(context, Config)._tag}`) -}) - -// ============================================ -// 6. Custom logger for development -// ============================================ - -const devLogger = Logger.make(({ logLevel, message, date, annotations, spans }) => { - const timestamp = date.toISOString() - const level = logLevel.label.padEnd(7) - const spanInfo = spans.length > 0 - ? ` [${[...spans].map(([name]) => name).join(" > ")}]` - : "" - const annotationInfo = Object.keys(annotations).length > 0 - ? ` ${JSON.stringify(Object.fromEntries(annotations))}` - : "" - - console.log(`${timestamp} ${level}${spanInfo} ${message}${annotationInfo}`) -}) - -const withDevLogger = (effect: Effect.Effect) => - effect.pipe( - Effect.provide(Logger.replace(Logger.defaultLogger, devLogger)) - ) - -// ============================================ -// 7. Runtime metrics -// ============================================ - -const showRuntimeMetrics = Effect.gen(function* () { - const runtime = yield* Effect.runtime() - - yield* Effect.log("=== Runtime Info ===") - // Access runtime configuration - const fiberRefs = runtime.fiberRefs - - yield* Effect.log("FiberRefs available") -}) - -// ============================================ -// 8. Putting it all together -// ============================================ - -const debugSession = Effect.gen(function* () { - yield* Effect.log("Starting debug session") - - // Run with all debugging enabled - yield* tracedProgram.pipe( - withDevLogger, - Logger.withMinimumLogLevel(LogLevel.Debug) - ) - - yield* debugErrors - - yield* Effect.log("Debug session complete") -}) - -Effect.runPromise(debugSession) -``` - -### Debug Output Example - -``` -2024-01-15T10:30:00.000Z DEBUG [initialization] Step 1: Initialize -2024-01-15T10:30:00.100Z DEBUG [processing] Step 2: Process -2024-01-15T10:30:00.300Z DEBUG [finalization] Step 3: Finalize -2024-01-15T10:30:00.350Z INFO Program complete -``` - -### Explanation - -Effect DevTools help you: - -1. **See fiber state** - What's running, blocked, completed -2. **Trace execution** - Follow the flow of effects -3. **Debug errors** - Understand failure chains -4. **Profile performance** - Find slow operations - ---- - ---- - -## Use Effect.gen for Business Logic - -**Rule:** Use Effect.gen for business logic. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good 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 timestamp = yield* Effect.sync(() => Date.now()); - const hashed = `hashed_${pw}_${timestamp}`; - 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. - -### Anti-Pattern - -Using long chains of `.andThen` or `.flatMap` for multi-step business logic. -This is harder to read and pass state between steps. - -### Explanation - -Generators provide a syntax that closely resembles standard synchronous code -(`async/await`), making complex workflows significantly easier to read, write, -and debug. - ---- - -## Use the Auto-Generated .Default Layer in Tests - -**Rule:** Use the auto-generated .Default layer in tests. - -**Skill Level:** intermediate - -**Use Cases:** testing - -### Good 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. - -### Anti-Pattern - -Do not create manual layers for your service in tests (`Layer.succeed(...)`) or try to provide the service class directly. This bypasses the intended dependency injection mechanism. - -### Explanation - -The `.Default` layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust. - ---- - -## Validate Request Body - -**Rule:** Use Http.request.schemaBodyJson with a Schema to automatically parse and validate request bodies. - -**Skill Level:** intermediate - -**Use Cases:** building-apis - -### Good 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 -*/ -``` - -### Anti-Pattern - -The anti-pattern is to manually parse the JSON and then write imperative validation checks. This approach is verbose, error-prone, and not type-safe. - -```typescript -import { Effect } from "effect"; -import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node"; - -const createUserRoute = Http.router.post( - "/users", - Http.request.json.pipe( - // Http.request.json returns Effect - Effect.flatMap((body) => { - // Manually check the type and properties of the body. - if ( - typeof body === "object" && - body !== null && - "name" in body && - typeof body.name === "string" && - "email" in body && - typeof body.email === "string" - ) { - // The type is still not safely inferred here without casting. - return Http.response.text(`Successfully created user: ${body.name}`); - } else { - // Manually create and return a generic error response. - return Http.response.text("Invalid request body", { status: 400 }); - } - }) - ) -); - -const app = Http.router.empty.pipe(Http.router.addRoute(createUserRoute)); - -const program = Http.server - .serve(app) - .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 }))); - -NodeRuntime.runMain(program); -``` - -This manual code is significantly worse. It's hard to read, easy to get wrong, and loses all static type information from the parsed body. Crucially, it forces you to reinvent the wheel for error reporting, which will likely be less detailed and consistent than the automatic responses provided by the platform. - -### Explanation - -Accepting user-provided data is one of the most critical and sensitive parts of an API. You must never trust incoming data. The `Http` module's integration with `Schema` provides a robust, declarative solution for this. - -Using `Http.request.schemaBodyJson` offers several major advantages: - -1. **Automatic Validation and Error Handling**: If the incoming body does not match the schema, the server automatically rejects the request with a `400 Bad Request` status and a detailed JSON response explaining the validation errors. You don't have to write any of this boilerplate logic. -2. **Type Safety**: If the validation succeeds, the value produced by the `Effect` is fully typed according to your `Schema`. This eliminates `any` types and brings static analysis benefits to your request handlers. -3. **Declarative and Clean**: The validation rules are defined once in the `Schema` and then simply applied. This separates the validation logic from your business logic, keeping handlers clean and focused on their core task. -4. **Security**: It acts as a security gateway, ensuring that malformed or unexpected data structures never reach your application's core logic. - ---- - ---- - -## Validating and Parsing Branded Types - -**Rule:** Combine Schema and Brand to validate and parse branded types, guaranteeing only valid domain values are created at runtime. - -**Skill Level:** intermediate - -**Use Cases:** domain-modeling - -### Good Example - -```typescript -import { Brand, Effect, Schema } from "effect"; - -// Define a branded type for Email -type Email = string & Brand.Brand<"Email">; - -// Create a Schema for Email validation -const EmailSchema = Schema.String.pipe( - Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/), // Simple email regex - Schema.brand("Email" as const) // Attach the brand -); - -// Parse and validate an email at runtime -const parseEmail = (input: string) => - Effect.try({ - try: () => Schema.decodeSync(EmailSchema)(input), - catch: (err) => `Invalid email: ${String(err)}`, - }); - -// Usage -parseEmail("user@example.com").pipe( - Effect.match({ - onSuccess: (email) => console.log("Valid email:", email), - onFailure: (err) => console.error(err), - }) -); -``` - -**Explanation:** - -- `Schema` is used to define validation logic for the branded type. -- `Brand.schema()` attaches the brand to the schema, so only validated values can be constructed as `Email`. -- This pattern ensures both compile-time and runtime safety. - -### Anti-Pattern - -Branding values without runtime validation, or accepting unvalidated user input as branded types, which can lead to invalid domain values and runtime bugs. - -### Explanation - -While branding types at the type level prevents accidental misuse, runtime validation is needed to ensure only valid values are constructed from user input, APIs, or external sources. - ---- - -## Why Effect? Comparing Effect to Promise - -**Rule:** Understand why Effect is better than raw Promises. - -**Skill Level:** beginner - -**Use Cases:** getting-started - ---- - -## Work with Arbitrary-Precision Numbers using BigDecimal - -**Rule:** Use BigDecimal to represent and compute with decimal numbers that require arbitrary precision, such as in finance or scientific domains. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { BigDecimal } from "effect"; - -// Create BigDecimal values -const a = BigDecimal.fromNumber(0.1); -const b = BigDecimal.fromNumber(0.2); - -// Add, subtract, multiply, divide -const sum = BigDecimal.sum(a, b); // BigDecimal(0.3) -const product = BigDecimal.multiply(a, b); // BigDecimal(0.02) - -// Compare values -const isEqual = BigDecimal.equals(sum, BigDecimal.fromNumber(0.3)); // true - -// Convert to string or number -const asString = BigDecimal.format(BigDecimal.normalize(sum)); // "0.3" -const asNumber = BigDecimal.unsafeToNumber(sum); // 0.3 -``` - -**Explanation:** - -- `BigDecimal` is immutable and supports precise decimal arithmetic. -- Use it for domains where rounding errors are unacceptable (e.g., finance, billing, scientific data). -- Avoids the pitfalls of floating-point math in JavaScript. - -### Anti-Pattern - -Using JavaScript's native `number` type for financial or scientific calculations, which can lead to rounding errors and loss of precision. - -### Explanation - -JavaScript's `number` type is a floating-point double, which can introduce subtle bugs in calculations that require exact decimal representation. -`BigDecimal` provides precise, immutable arithmetic for critical domains. - ---- - -## Work with Dates and Times using DateTime - -**Rule:** Use DateTime to represent and manipulate dates and times in a type-safe, immutable, and time-zone-aware way. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { DateTime } from "effect"; - -// Create a DateTime for the current instant (returns an Effect) -import { Effect } from "effect"; - -const program = Effect.gen(function* () { - const now = yield* DateTime.now; // DateTime.Utc - - // Parse from ISO string - const parsed = DateTime.unsafeMakeZoned("2024-07-19T12:34:56Z"); // DateTime.Zoned - - // Add or subtract durations - const inOneHour = DateTime.add(now, { hours: 1 }); - const oneHourAgo = DateTime.subtract(now, { hours: 1 }); - - // Format as ISO string - const iso = DateTime.formatIso(now); // e.g., "2024-07-19T23:33:19.000Z" - - // Compare DateTimes - const isBefore = DateTime.lessThan(oneHourAgo, now); // true - - return { now, inOneHour, oneHourAgo, iso, isBefore }; -}); -``` - -**Explanation:** - -- `DateTime` is immutable and time-zone-aware. -- Supports parsing, formatting, arithmetic, and comparison. -- Use for all date/time logic to avoid bugs with native `Date`. - -### Anti-Pattern - -Using JavaScript's mutable `Date` for time calculations, or ignoring time zones, which can lead to subtle and hard-to-debug errors. - -### Explanation - -JavaScript's native `Date` is mutable, not time-zone-aware, and can be error-prone. -`DateTime` provides an immutable, functional alternative with explicit time zone handling and robust APIs for time arithmetic. - ---- - -## Work with Immutable Sets using HashSet - -**Rule:** Use HashSet to represent sets of unique values with efficient, immutable operations for membership, union, intersection, and difference. - -**Skill Level:** intermediate - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { HashSet } from "effect"; - -// Create a HashSet from an array -const setA = HashSet.fromIterable([1, 2, 3]); -const setB = HashSet.fromIterable([3, 4, 5]); - -// Membership check -const hasTwo = HashSet.has(setA, 2); // true - -// Union, intersection, difference -const union = HashSet.union(setA, setB); // HashSet {1, 2, 3, 4, 5} -const intersection = HashSet.intersection(setA, setB); // HashSet {3} -const difference = HashSet.difference(setA, setB); // HashSet {1, 2} - -// Add and remove elements -const withSix = HashSet.add(setA, 6); // HashSet {1, 2, 3, 6} -const withoutOne = HashSet.remove(setA, 1); // HashSet {2, 3} -``` - -**Explanation:** - -- `HashSet` is immutable and supports efficient set operations. -- Use it for membership checks, set algebra, and modeling unique collections. -- Safe for concurrent and functional workflows. - -### Anti-Pattern - -Using mutable JavaScript `Set` for shared or concurrent data, or for set operations in functional code, which can lead to bugs and unpredictable behavior. - -### Explanation - -`HashSet` provides high-performance, immutable set operations that are safe for concurrent and functional programming. -It avoids the pitfalls of mutable JavaScript `Set` and is optimized for use in Effect workflows. - ---- - -## Working with Immutable Arrays using Data.array - -**Rule:** Use Data.array to define arrays whose equality is based on their contents, enabling safe, predictable comparisons and functional operations. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Data, Equal } from "effect"; - -// Create two structurally equal arrays -const arr1 = Data.array([1, 2, 3]); -const arr2 = Data.array([1, 2, 3]); - -// Compare by value, not reference -const areEqual = Equal.equals(arr1, arr2); // true - -// Use arrays as keys in a HashSet or Map -import { HashSet } from "effect"; -const set = HashSet.make(arr1); -console.log(HashSet.has(set, arr2)); // true - -// Functional operations (map, filter, etc.) -const doubled = arr1.map((n) => n * 2); // Data.array([2, 4, 6]) -``` - -**Explanation:** - -- `Data.array` creates immutable arrays with value-based equality. -- Useful for modeling ordered collections in a safe, functional way. -- Supports all standard array operations, but with immutability and structural equality. - -### Anti-Pattern - -Using plain JavaScript arrays for value-based logic, as keys in sets/maps, or in concurrent code, which can lead to bugs due to mutability and reference-based comparison. - -### Explanation - -JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic and concurrent code. -`Data.array` provides immutable arrays with structural equality, making them ideal for functional programming and safe domain modeling. - ---- - -## Working with Tuples using Data.tuple - -**Rule:** Use Data.tuple to define tuples whose equality is based on their contents, enabling safe and predictable comparisons and pattern matching. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Data, Equal } from "effect"; - -// Create two structurally equal tuples -const t1 = Data.tuple(1, "Alice"); -const t2 = Data.tuple(1, "Alice"); - -// Compare by value, not reference -const areEqual = Equal.equals(t1, t2); // true - -// Use tuples as keys in a HashSet or Map -import { HashSet } from "effect"; -const set = HashSet.make(t1); -console.log(HashSet.has(set, t2)); // true - -// Pattern matching on tuples -const [id, name] = t1; // id: number, name: string -``` - -**Explanation:** - -- `Data.tuple` creates immutable tuples with value-based equality. -- Useful for modeling pairs, coordinates, or any fixed-size, heterogeneous data. -- Supports safe pattern matching and collection operations. - -### Anti-Pattern - -Using plain arrays for value-based logic or as keys in sets/maps, which compares by reference and can lead to incorrect behavior. - -### Explanation - -JavaScript arrays are mutable and compared by reference, which can lead to bugs in value-based logic. -`Data.tuple` provides immutable tuples with structural equality, making them ideal for domain modeling and functional programming patterns. - ---- - -## Wrap Asynchronous Computations with tryPromise - -**Rule:** Wrap asynchronous computations with tryPromise. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect, Data } from "effect"; - -// Define error type using Data.TaggedError -class HttpError extends Data.TaggedError("HttpError")<{ - readonly message: string; -}> {} - -// Define HTTP client service -export class HttpClient extends Effect.Service()("HttpClient", { - // Provide default implementation - sync: () => ({ - getUrl: (url: string) => - Effect.tryPromise({ - try: () => fetch(url), - catch: (error) => - new HttpError({ message: `Failed to fetch ${url}: ${error}` }), - }), - }), -}) {} - -// Mock HTTP client for demonstration -export class MockHttpClient extends Effect.Service()( - "MockHttpClient", - { - sync: () => ({ - getUrl: (url: string) => - Effect.gen(function* () { - yield* Effect.logInfo(`Fetching URL: ${url}`); - - // Simulate different responses based on URL - if (url.includes("success")) { - yield* Effect.logInfo("✅ Request successful"); - return new Response(JSON.stringify({ data: "success" }), { - status: 200, - }); - } else if (url.includes("error")) { - yield* Effect.logInfo("❌ Request failed"); - return yield* Effect.fail( - new HttpError({ message: "Server returned 500" }) - ); - } else { - yield* Effect.logInfo("✅ Request completed"); - return new Response(JSON.stringify({ data: "mock response" }), { - status: 200, - }); - } - }), - }), - } -) {} - -// Demonstrate wrapping asynchronous computations -const program = Effect.gen(function* () { - yield* Effect.logInfo("=== Wrapping Asynchronous Computations Demo ==="); - - const client = yield* MockHttpClient; - - // Example 1: Successful request - yield* Effect.logInfo("\n1. Successful request:"); - const response1 = yield* client - .getUrl("https://api.example.com/success") - .pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Request failed: ${error.message}`); - return new Response("Error response", { status: 500 }); - }) - ) - ); - yield* Effect.logInfo(`Response status: ${response1.status}`); - - // Example 2: Failed request with error handling - yield* Effect.logInfo("\n2. Failed request with error handling:"); - const response2 = yield* client.getUrl("https://api.example.com/error").pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Request failed: ${error.message}`); - return new Response("Fallback response", { status: 200 }); - }) - ) - ); - yield* Effect.logInfo(`Fallback response status: ${response2.status}`); - - // Example 3: Multiple async operations - yield* Effect.logInfo("\n3. Multiple async operations:"); - const results = yield* Effect.all( - [ - client.getUrl("https://api.example.com/endpoint1"), - client.getUrl("https://api.example.com/endpoint2"), - client.getUrl("https://api.example.com/endpoint3"), - ], - { concurrency: 2 } - ).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`One or more requests failed: ${error.message}`); - return []; - }) - ) - ); - yield* Effect.logInfo(`Completed ${results.length} requests`); - - yield* Effect.logInfo( - "\n✅ Asynchronous computations demonstration completed!" - ); -}); - -// Run with mock implementation -Effect.runPromise(Effect.provide(program, MockHttpClient.Default)); -``` - -**Explanation:** -`Effect.tryPromise` wraps a `Promise`-returning function and safely handles -rejections, moving errors into the Effect's error channel. - -### Anti-Pattern - -Manually handling `.then()` and `.catch()` inside an `Effect.sync`. This is -verbose, error-prone, and defeats the purpose of using Effect's built-in -Promise integration. - -### Explanation - -This is the standard bridge from the Promise-based world to Effect, allowing -you to leverage the massive `async/await` ecosystem safely. - ---- - -## Wrap Synchronous Computations with sync and try - -**Rule:** Wrap synchronous computations with sync and try. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -const randomNumber = Effect.sync(() => Math.random()); - -const parseJson = (input: string) => - Effect.try({ - try: () => JSON.parse(input), - catch: (error) => new Error(`JSON parsing failed: ${error}`), - }); - -// More examples of wrapping synchronous computations -const divide = (a: number, b: number) => - Effect.try({ - try: () => { - if (b === 0) throw new Error("Division by zero"); - return a / b; - }, - catch: (error) => new Error(`Division failed: ${error}`), - }); - -const processString = (str: string) => - Effect.gen(function* () { - yield* Effect.log(`Processing string: "${str}"`); - return str.toUpperCase().split("").reverse().join(""); - }); - -// Demonstrate wrapping synchronous computations -const program = Effect.gen(function* () { - yield* Effect.log("=== Wrapping Synchronous Computations Demo ==="); - - // Example 1: Basic sync computation - yield* Effect.log("\n1. Basic sync computation (random number):"); - const random1 = yield* randomNumber; - const random2 = yield* randomNumber; - yield* Effect.log( - `Random numbers: ${random1.toFixed(4)}, ${random2.toFixed(4)}` - ); - - // Example 2: Successful JSON parsing - yield* Effect.log("\n2. Successful JSON parsing:"); - const validJson = '{"name": "Paul", "age": 30}'; - const parsed = yield* parseJson(validJson); - yield* Effect.log("Parsed JSON:" + JSON.stringify(parsed)); - - // Example 3: Failed JSON parsing with error logging - yield* Effect.log("\n3. Failed JSON parsing with error logging:"); - const invalidJson = '{"name": "Paul", "age":}'; - yield* parseJson(invalidJson).pipe( - Effect.tapError((error) => Effect.log(`Parsing failed: ${error.message}`)), - Effect.catchAll(() => Effect.succeed({ name: "default", age: 0 })) - ); - yield* Effect.log("Continued after error (with recovery)"); - - // Example 4: Division with error logging and recovery - yield* Effect.log("\n4. Division with error logging and recovery:"); - const division1 = yield* divide(10, 2); - yield* Effect.log(`10 / 2 = ${division1}`); - - // Use tapError to log, then catchAll to recover - const division2 = yield* divide(10, 0).pipe( - Effect.tapError((error) => Effect.log(`Division error: ${error.message}`)), - Effect.catchAll(() => Effect.succeed(-1)) - ); - yield* Effect.log(`10 / 0 = ${division2} (error handled)`); - - // Example 5: String processing - yield* Effect.log("\n5. String processing:"); - const processed = yield* processString("Hello Effect"); - yield* Effect.log(`Processed result: "${processed}"`); - - // Example 6: Combining multiple sync operations - yield* Effect.log("\n6. Combining multiple sync operations:"); - const combined = yield* Effect.gen(function* () { - const num = yield* randomNumber; - const multiplied = yield* Effect.sync(() => num * 100); - const rounded = yield* Effect.sync(() => Math.round(multiplied)); - return rounded; - }); - yield* Effect.log(`Combined operations result: ${combined}`); - - yield* Effect.log("\n✅ Synchronous computations demonstration completed!"); -}); - -Effect.runPromise(program); -``` - -**Explanation:** -Use `Effect.sync` for safe synchronous code, and `Effect.try` to safely -handle exceptions from potentially unsafe code. - -### Anti-Pattern - -Never use `Effect.sync` for an operation that could throw, like `JSON.parse`. -This can lead to unhandled exceptions that crash your application. - -### Explanation - -This is the primary way to safely integrate with synchronous libraries like -`JSON.parse`. `Effect.try` captures any thrown exception and moves it into -the Effect's error channel. - ---- - -## Wrapping Synchronous and Asynchronous Computations - -**Rule:** Use try and tryPromise to lift code that may throw or reject into Effect, capturing errors in the failure channel. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Synchronous: Wrap code that may throw -const effectSync = Effect.try({ - try: () => JSON.parse("{ invalid json }"), - catch: (error) => `Parse error: ${String(error)}`, -}); // Effect - -// Asynchronous: Wrap a promise that may reject -const effectAsync = Effect.tryPromise({ - try: () => fetch("https://api.example.com/data").then((res) => res.json()), - catch: (error) => `Network error: ${String(error)}`, -}); // Effect -``` - -**Explanation:** - -- `Effect.try` wraps a synchronous computation that may throw, capturing the error in the failure channel. -- `Effect.tryPromise` wraps an async computation (Promise) that may reject, capturing the rejection as a failure. - -### Anti-Pattern - -Using try/catch for error handling, or relying on untyped Promise rejections, which leads to less composable and less type-safe code. - -### Explanation - -Wrapping potentially unsafe code in `try` or `tryPromise` ensures that all errors are handled in a uniform, declarative way. -This eliminates the need for try/catch blocks and makes error handling explicit and type-safe. - ---- - -## Write Sequential Code with Effect.gen - -**Rule:** Write sequential code with Effect.gen. - -**Skill Level:** beginner - -**Use Cases:** core-concepts - -### Good Example - -```typescript -import { Effect } from "effect"; - -// Mock API functions for demonstration -const fetchUser = (id: number) => - Effect.gen(function* () { - yield* Effect.logInfo(`Fetching user ${id}...`); - // Simulate API call - yield* Effect.sleep("100 millis"); - return { id, name: `User ${id}`, email: `user${id}@example.com` }; - }); - -const fetchUserPosts = (userId: number) => - Effect.gen(function* () { - yield* Effect.logInfo(`Fetching posts for user ${userId}...`); - // Simulate API call - yield* Effect.sleep("150 millis"); - return [ - { id: 1, title: "First Post", userId }, - { id: 2, title: "Second Post", userId }, - ]; - }); - -const fetchPostComments = (postId: number) => - Effect.gen(function* () { - yield* Effect.logInfo(`Fetching comments for post ${postId}...`); - // Simulate API call - yield* Effect.sleep("75 millis"); - return [ - { id: 1, text: "Great post!", postId }, - { id: 2, text: "Thanks for sharing", postId }, - ]; - }); - -// Example of sequential code with Effect.gen -const getUserDataWithGen = (userId: number) => - Effect.gen(function* () { - // Step 1: Fetch user - const user = yield* fetchUser(userId); - yield* Effect.logInfo(`✅ Got user: ${user.name}`); - - // Step 2: Fetch user's posts (depends on user data) - const posts = yield* fetchUserPosts(user.id); - yield* Effect.logInfo(`✅ Got ${posts.length} posts`); - - // Step 3: Fetch comments for first post (depends on posts data) - const firstPost = posts[0]; - const comments = yield* fetchPostComments(firstPost.id); - yield* Effect.logInfo( - `✅ Got ${comments.length} comments for "${firstPost.title}"` - ); - - // Step 4: Combine all data - const result = { - user, - posts, - featuredPost: { - ...firstPost, - comments, - }, - }; - - yield* Effect.logInfo("✅ Successfully combined all user data"); - return result; - }); - -// Example without Effect.gen (more complex) -const getUserDataWithoutGen = (userId: number) => - fetchUser(userId).pipe( - Effect.flatMap((user) => - fetchUserPosts(user.id).pipe( - Effect.flatMap((posts) => - fetchPostComments(posts[0].id).pipe( - Effect.map((comments) => ({ - user, - posts, - featuredPost: { - ...posts[0], - comments, - }, - })) - ) - ) - ) - ) - ); - -// Demonstrate writing sequential code with gen -const program = Effect.gen(function* () { - yield* Effect.logInfo("=== Writing Sequential Code with Effect.gen Demo ==="); - - // Example 1: Sequential operations with Effect.gen - yield* Effect.logInfo("\n1. Sequential operations with Effect.gen:"); - const userData = yield* getUserDataWithGen(123).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Failed to get user data: ${error}`); - return null; - }) - ) - ); - - if (userData) { - yield* Effect.logInfo( - `Final result: User "${userData.user.name}" has ${userData.posts.length} posts` - ); - yield* Effect.logInfo( - `Featured post: "${userData.featuredPost.title}" with ${userData.featuredPost.comments.length} comments` - ); - } - - // Example 2: Compare with traditional promise-like chaining - yield* Effect.logInfo("\n2. Same logic without Effect.gen (for comparison):"); - const userData2 = yield* getUserDataWithoutGen(456).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Failed to get user data: ${error}`); - return null; - }) - ) - ); - - if (userData2) { - yield* Effect.logInfo( - `Result from traditional approach: User "${userData2.user.name}"` - ); - } - - // Example 3: Error handling in sequential code - yield* Effect.logInfo("\n3. Error handling in sequential operations:"); - const errorHandling = yield* Effect.gen(function* () { - try { - const user = yield* fetchUser(999); - const posts = yield* fetchUserPosts(user.id); - return { user, posts }; - } catch (error) { - yield* Effect.logError(`Error in sequential operations: ${error}`); - return null; - } - }).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Effect.logError(`Caught error: ${error}`); - return { user: null, posts: [] }; - }) - ) - ); - - yield* Effect.logInfo( - `Error handling result: ${errorHandling ? "Success" : "Handled error"}` - ); - - yield* Effect.logInfo("\n✅ Sequential code demonstration completed!"); - yield* Effect.logInfo( - "Effect.gen makes sequential async code look like synchronous code!" - ); -}); - -Effect.runPromise(program); -``` - -**Explanation:** -`Effect.gen` allows you to write top-to-bottom code that is easy to read and -maintain, even when chaining many asynchronous steps. - -### Anti-Pattern - -Deeply nesting `flatMap` calls. This is much harder to read and maintain than -the equivalent `Effect.gen` block. - -### Explanation - -`Effect.gen` uses generator functions to create a flat, linear, and highly -readable sequence of operations, avoiding the nested "callback hell" of -`flatMap`. - ---- - -## Write Tests That Adapt to Application Code - -**Rule:** Write tests that adapt to application code. - -**Skill Level:** intermediate - -**Use Cases:** testing - -### Good 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. - -### Anti-Pattern - -Any action where the test dictates a change to the application code. Do not modify a service file to add a method just because a test needs it. If a test fails, fix the test. - -### Explanation - -Treating application code as immutable during testing prevents the introduction of bugs and false test confidence. The goal of a test is to verify real-world behavior; changing that behavior to suit the test invalidates its purpose. - ---- - -## Your First Domain Model - -**Rule:** Start domain modeling by defining clear interfaces for your business entities. - -**Skill Level:** beginner - -**Use Cases:** domain-modeling - -### Good Example - -```typescript -import { Effect } from "effect" - -// ============================================ -// 1. Define domain entities as interfaces -// ============================================ - -interface User { - readonly id: string - readonly email: string - readonly name: string - readonly createdAt: Date -} - -interface Product { - readonly sku: string - readonly name: string - readonly price: number - readonly inStock: boolean -} - -interface Order { - readonly id: string - readonly userId: string - readonly items: ReadonlyArray - readonly total: number - readonly status: OrderStatus -} - -interface OrderItem { - readonly productSku: string - readonly quantity: number - readonly unitPrice: number -} - -type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" - -// ============================================ -// 2. Create domain functions -// ============================================ - -const createUser = (email: string, name: string): User => ({ - id: crypto.randomUUID(), - email, - name, - createdAt: new Date(), -}) - -const calculateOrderTotal = (items: ReadonlyArray): number => - items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0) - -// ============================================ -// 3. Use in Effect programs -// ============================================ - -const program = Effect.gen(function* () { - const user = createUser("alice@example.com", "Alice") - yield* Effect.log(`Created user: ${user.name}`) - - const items: OrderItem[] = [ - { productSku: "WIDGET-001", quantity: 2, unitPrice: 29.99 }, - { productSku: "GADGET-002", quantity: 1, unitPrice: 49.99 }, - ] - - const order: Order = { - id: crypto.randomUUID(), - userId: user.id, - items, - total: calculateOrderTotal(items), - status: "pending", - } - - yield* Effect.log(`Order total: $${order.total.toFixed(2)}`) - return order -}) - -Effect.runPromise(program) -``` - -### Explanation - -Good domain modeling: - -1. **Clarifies intent** - Types document what data means -2. **Prevents errors** - Compiler catches wrong data usage -3. **Enables tooling** - IDE autocompletion and refactoring -4. **Communicates** - Code becomes documentation - ---- - ---- - -## Your First Effect Test - -**Rule:** Use Effect.runPromise in tests to run and assert on Effect results. - -**Skill Level:** beginner - -**Use Cases:** testing - -### Good Example - -```typescript -import { describe, it, expect } from "vitest" -import { Effect } from "effect" - -// ============================================ -// Code to test -// ============================================ - -const add = (a: number, b: number): Effect.Effect => - Effect.succeed(a + b) - -const divide = (a: number, b: number): Effect.Effect => - b === 0 - ? Effect.fail(new Error("Cannot divide by zero")) - : Effect.succeed(a / b) - -const fetchUser = (id: string): Effect.Effect<{ id: string; name: string }> => - Effect.succeed({ id, name: `User ${id}` }) - -// ============================================ -// Tests -// ============================================ - -describe("Basic Effect Tests", () => { - it("should add two numbers", async () => { - const result = await Effect.runPromise(add(2, 3)) - expect(result).toBe(5) - }) - - it("should divide numbers", async () => { - const result = await Effect.runPromise(divide(10, 2)) - expect(result).toBe(5) - }) - - it("should fail on divide by zero", async () => { - await expect(Effect.runPromise(divide(10, 0))).rejects.toThrow( - "Cannot divide by zero" - ) - }) - - it("should fetch a user", async () => { - const user = await Effect.runPromise(fetchUser("123")) - - expect(user).toEqual({ - id: "123", - name: "User 123", - }) - }) -}) - -// ============================================ -// Testing Effect.gen programs -// ============================================ - -const calculateDiscount = (price: number, quantity: number) => - Effect.gen(function* () { - if (price <= 0) { - return yield* Effect.fail(new Error("Invalid price")) - } - - const subtotal = price * quantity - const discount = quantity >= 10 ? 0.1 : 0 - const total = subtotal * (1 - discount) - - return { subtotal, discount, total } - }) - -describe("Effect.gen Tests", () => { - it("should calculate without discount", async () => { - const result = await Effect.runPromise(calculateDiscount(10, 5)) - - expect(result.subtotal).toBe(50) - expect(result.discount).toBe(0) - expect(result.total).toBe(50) - }) - - it("should apply bulk discount", async () => { - const result = await Effect.runPromise(calculateDiscount(10, 10)) - - expect(result.subtotal).toBe(100) - expect(result.discount).toBe(0.1) - expect(result.total).toBe(90) - }) - - it("should fail for invalid price", async () => { - await expect( - Effect.runPromise(calculateDiscount(-5, 10)) - ).rejects.toThrow("Invalid price") - }) -}) -``` - -### Explanation - -Testing Effect code is straightforward: - -1. **Effects are values** - Build them in tests like any other value -2. **Run to get results** - Use `Effect.runPromise` to execute -3. **Assert normally** - Standard assertions work on the results - ---- - ---- - -## Your First Error Handler - -**Rule:** Use catchAll or catchTag to recover from errors and keep your program running. - -**Skill Level:** beginner - -**Use Cases:** error-management - -### Good Example - -```typescript -import { Effect, Data } from "effect" - -// ============================================ -// 1. Define typed errors -// ============================================ - -class NetworkError extends Data.TaggedError("NetworkError")<{ - readonly url: string -}> {} - -class NotFoundError extends Data.TaggedError("NotFoundError")<{ - readonly resource: string -}> {} - -// ============================================ -// 2. Functions that can fail -// ============================================ - -const fetchData = (url: string): Effect.Effect => - url.startsWith("http") - ? Effect.succeed(`Data from ${url}`) - : Effect.fail(new NetworkError({ url })) - -const findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> => - id === "123" - ? Effect.succeed({ id, name: "Alice" }) - : Effect.fail(new NotFoundError({ resource: `user:${id}` })) - -// ============================================ -// 3. Handle ALL errors with catchAll -// ============================================ - -const withFallback = fetchData("invalid-url").pipe( - Effect.catchAll((error) => { - console.log(`Failed: ${error.url}, using fallback`) - return Effect.succeed("Fallback data") - }) -) - -// Result: "Fallback data" - -// ============================================ -// 4. Handle SPECIFIC errors with catchTag -// ============================================ - -const findUserOrDefault = (id: string) => - findUser(id).pipe( - Effect.catchTag("NotFoundError", (error) => { - console.log(`User not found: ${error.resource}`) - return Effect.succeed({ id: "guest", name: "Guest User" }) - }) - ) - -// ============================================ -// 5. Handle MULTIPLE error types -// ============================================ - -const fetchUser = (url: string, id: string) => - Effect.gen(function* () { - yield* fetchData(url) - return yield* findUser(id) - }) - -const robustFetchUser = (url: string, id: string) => - fetchUser(url, id).pipe( - Effect.catchTags({ - NetworkError: (e) => Effect.succeed({ id: "offline", name: `Offline (${e.url})` }), - NotFoundError: (e) => Effect.succeed({ id: "unknown", name: `Unknown (${e.resource})` }), - }) - ) - -// ============================================ -// 6. Run the examples -// ============================================ - -const program = Effect.gen(function* () { - // catchAll example - const data = yield* withFallback - yield* Effect.log(`Got data: ${data}`) - - // catchTag example - const user = yield* findUserOrDefault("999") - yield* Effect.log(`Got user: ${user.name}`) - - // Multiple error types - const result = yield* robustFetchUser("invalid", "999") - yield* Effect.log(`Robust result: ${result.name}`) -}) - -Effect.runPromise(program) -``` - -### Explanation - -Effect makes errors explicit in your types: - -1. **Errors are typed** - You know exactly what can fail -2. **Handle or propagate** - Can't accidentally ignore errors -3. **Recovery options** - Provide fallbacks, retry, or transform -4. **No try/catch** - Declarative error handling - ---- - ---- - -## Your First HTTP Request - -**Rule:** Use @effect/platform HttpClient for type-safe HTTP requests with automatic error handling. - -**Skill Level:** beginner - -**Use Cases:** making-http-requests - -### Good Example - -```typescript -import { Effect, Console } from "effect" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" -import { NodeHttpClient, NodeRuntime } from "@effect/platform-node" - -// ============================================ -// 1. Simple GET request -// ============================================ - -const simpleGet = Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - // Make a GET request - const response = yield* client.get("https://jsonplaceholder.typicode.com/posts/1") - - // Get response as JSON - const json = yield* HttpClientResponse.json(response) - - return json -}) - -// ============================================ -// 2. GET with typed response -// ============================================ - -interface Post { - id: number - title: string - body: string - userId: number -} - -const getPost = (id: number) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - const response = yield* client.get( - `https://jsonplaceholder.typicode.com/posts/${id}` - ) - const post = yield* HttpClientResponse.json(response) as Effect.Effect - return post - }) - -// ============================================ -// 3. POST with body -// ============================================ - -const createPost = (title: string, body: string) => - Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - - const request = HttpClientRequest.post( - "https://jsonplaceholder.typicode.com/posts" - ).pipe( - HttpClientRequest.jsonBody({ title, body, userId: 1 }) - ) - - const response = yield* client.execute(yield* request) - const created = yield* HttpClientResponse.json(response) - - return created - }) - -// ============================================ -// 4. Handle errors -// ============================================ - -const safeGetPost = (id: number) => - getPost(id).pipe( - Effect.catchAll((error) => - Effect.gen(function* () { - yield* Console.error(`Failed to fetch post ${id}: ${error}`) - return { id, title: "Unavailable", body: "", userId: 0 } - }) - ) - ) - -// ============================================ -// 5. Run the program -// ============================================ - -const program = Effect.gen(function* () { - yield* Console.log("=== Simple GET ===") - const data = yield* simpleGet - yield* Console.log(JSON.stringify(data, null, 2)) - - yield* Console.log("\n=== Typed GET ===") - const post = yield* getPost(1) - yield* Console.log(`Post: ${post.title}`) - - yield* Console.log("\n=== POST Request ===") - const created = yield* createPost("My New Post", "This is the body") - yield* Console.log(`Created: ${JSON.stringify(created)}`) -}) - -// Provide the HTTP client implementation -program.pipe( - Effect.provide(NodeHttpClient.layer), - NodeRuntime.runMain -) -``` - -### Explanation - -Effect's HttpClient is better than `fetch`: - -1. **Type-safe errors** - Network failures are typed, not exceptions -2. **Automatic JSON parsing** - No manual `.json()` calls -3. **Composable** - Chain requests, add retries, timeouts -4. **Testable** - Easy to mock in tests - ---- - ---- - -## Your First Logs - -**Rule:** Use Effect.log and related functions for structured, contextual logging. - -**Skill Level:** beginner - -**Use Cases:** observability - -### Good Example - -```typescript -import { Effect, Logger, LogLevel } from "effect" - -// ============================================ -// 1. Basic logging -// ============================================ - -const basicLogging = Effect.gen(function* () { - // Different log levels - yield* Effect.logDebug("Debug message - for development") - yield* Effect.logInfo("Info message - normal operation") - yield* Effect.log("Default log - same as logInfo") - yield* Effect.logWarning("Warning - something unusual") - yield* Effect.logError("Error - something went wrong") -}) - -// ============================================ -// 2. Logging with context -// ============================================ - -const withContext = Effect.gen(function* () { - // Add structured data to logs - yield* Effect.log("User logged in").pipe( - Effect.annotateLogs({ - userId: "user-123", - action: "login", - ipAddress: "192.168.1.1", - }) - ) - - // Add a single annotation - yield* Effect.log("Processing request").pipe( - Effect.annotateLogs("requestId", "req-456") - ) -}) - -// ============================================ -// 3. Log spans for timing -// ============================================ - -const withTiming = Effect.gen(function* () { - yield* Effect.log("Starting operation") - - // withLogSpan adds timing information - yield* Effect.sleep("100 millis").pipe( - Effect.withLogSpan("database-query") - ) - - yield* Effect.log("Operation complete") -}) - -// ============================================ -// 4. Practical example -// ============================================ - -interface User { - id: string - email: string -} - -const processOrder = (orderId: string, userId: string) => - Effect.gen(function* () { - yield* Effect.logInfo("Processing order").pipe( - Effect.annotateLogs({ orderId, userId }) - ) - - // Simulate work - yield* Effect.sleep("50 millis") - - yield* Effect.logInfo("Order processed successfully").pipe( - Effect.annotateLogs({ orderId, status: "completed" }) - ) - - return { orderId, status: "completed" } - }).pipe( - Effect.withLogSpan("processOrder") - ) - -// ============================================ -// 5. Configure log level -// ============================================ - -const debugProgram = basicLogging.pipe( - // Show all logs including debug - Logger.withMinimumLogLevel(LogLevel.Debug) -) - -const productionProgram = basicLogging.pipe( - // Only show warnings and errors - Logger.withMinimumLogLevel(LogLevel.Warning) -) - -// ============================================ -// 6. Run -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("=== Basic Logging ===") - yield* basicLogging - - yield* Effect.log("\n=== With Context ===") - yield* withContext - - yield* Effect.log("\n=== With Timing ===") - yield* withTiming - - yield* Effect.log("\n=== Process Order ===") - yield* processOrder("order-789", "user-123") -}) - -Effect.runPromise(program) -``` - -### Explanation - -Effect's logging is superior to `console.log`: - -1. **Structured** - Logs are data, not just strings -2. **Contextual** - Automatically includes fiber info, timestamps -3. **Configurable** - Change log levels, formats, destinations -4. **Type-safe** - Part of the Effect type system - ---- - ---- - -## Your First Parallel Operation - -**Rule:** Use Effect.all with concurrency option to run independent effects in parallel. - -**Skill Level:** beginner - -**Use Cases:** concurrency-getting-started - -### Good Example - -```typescript -import { Effect } from "effect" - -// Simulate async operations -const fetchUser = Effect.gen(function* () { - yield* Effect.sleep("100 millis") - return { id: 1, name: "Alice" } -}) - -const fetchProducts = Effect.gen(function* () { - yield* Effect.sleep("150 millis") - return [{ id: 1, name: "Widget" }, { id: 2, name: "Gadget" }] -}) - -const fetchCart = Effect.gen(function* () { - yield* Effect.sleep("80 millis") - return { items: 3, total: 99.99 } -}) - -// ============================================ -// SEQUENTIAL: One after another (~330ms) -// ============================================ - -const sequential = Effect.all([fetchUser, fetchProducts, fetchCart]) - -// ============================================ -// PARALLEL: All at once (~150ms) -// ============================================ - -const parallel = Effect.all( - [fetchUser, fetchProducts, fetchCart], - { concurrency: "unbounded" } -) - -// ============================================ -// PARALLEL WITH LIMIT: Max 2 at a time -// ============================================ - -const limited = Effect.all( - [fetchUser, fetchProducts, fetchCart], - { concurrency: 2 } -) - -// ============================================ -// DEMO -// ============================================ - -const demo = Effect.gen(function* () { - const start = Date.now() - - const [user, products, cart] = yield* parallel - - const elapsed = Date.now() - start - yield* Effect.log(`Fetched in ${elapsed}ms`) - yield* Effect.log(`User: ${user.name}`) - yield* Effect.log(`Products: ${products.length}`) - yield* Effect.log(`Cart total: $${cart.total}`) -}) - -Effect.runPromise(demo) -// Output: Fetched in ~150ms (not ~330ms!) -``` - -### Explanation - -Parallel execution speeds up independent operations: - -1. **Fetch multiple APIs** - Get user, products, cart simultaneously -2. **Process files** - Read multiple files at once -3. **Database queries** - Run independent queries in parallel - ---- - ---- - -## Your First Platform Operation - -**Rule:** Use @effect/platform for cross-platform system operations with Effect integration. - -**Skill Level:** beginner - -**Use Cases:** platform-getting-started - -### Good Example - -```typescript -import { Effect } from "effect" -import { FileSystem } from "@effect/platform" -import { NodeContext, NodeRuntime } from "@effect/platform-node" - -// Read a file - returns Effect -const readConfig = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - - // Read file as UTF-8 string - const content = yield* fs.readFileString("./config.json") - - return JSON.parse(content) -}) - -// Write a file -const writeLog = Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem - - yield* fs.writeFileString( - "./app.log", - `Started at ${new Date().toISOString()}\n` - ) -}) - -// Combine operations -const program = Effect.gen(function* () { - const config = yield* readConfig - yield* Effect.log(`Loaded config: ${config.appName}`) - - yield* writeLog - yield* Effect.log("Log file created") -}) - -// Run with Node.js platform -program.pipe( - Effect.provide(NodeContext.layer), - NodeRuntime.runMain -) -``` - -### Explanation - -Platform wraps system operations in Effect, giving you: - -1. **Type safety** - File operations return `Effect` -2. **Resource management** - Files are automatically closed -3. **Cross-platform** - Same code works on Node.js, Bun, browser -4. **Composability** - Chain file ops with other effects - ---- - ---- - -## Your First Schedule - -**Rule:** Use Schedule to control when and how often effects run. - -**Skill Level:** beginner - -**Use Cases:** scheduling - -### Good Example - -```typescript -import { Effect, Schedule } from "effect" - -// ============================================ -// 1. Retry a failing operation -// ============================================ - -let attempts = 0 -const flakyOperation = Effect.gen(function* () { - attempts++ - if (attempts < 3) { - yield* Effect.log(`Attempt ${attempts} failed`) - return yield* Effect.fail(new Error("Temporary failure")) - } - return `Success on attempt ${attempts}` -}) - -// Retry up to 5 times -const withRetry = flakyOperation.pipe( - Effect.retry(Schedule.recurs(5)) -) - -// ============================================ -// 2. Repeat a successful operation -// ============================================ - -const logTime = Effect.gen(function* () { - const now = new Date().toISOString() - yield* Effect.log(`Current time: ${now}`) - return now -}) - -// Repeat 3 times -const repeated = logTime.pipe( - Effect.repeat(Schedule.recurs(3)) -) - -// ============================================ -// 3. Add delays between operations -// ============================================ - -// Repeat every second, 5 times -const polling = logTime.pipe( - Effect.repeat( - Schedule.spaced("1 second").pipe( - Schedule.intersect(Schedule.recurs(5)) - ) - ) -) - -// ============================================ -// 4. Common schedule patterns -// ============================================ - -// Fixed delay between attempts -const fixedDelay = Schedule.spaced("500 millis") - -// Increasing delay (1s, 2s, 4s, 8s...) -const exponentialBackoff = Schedule.exponential("1 second") - -// Maximum number of attempts -const limitedAttempts = Schedule.recurs(3) - -// Combine: exponential backoff, max 5 attempts -const retryPolicy = Schedule.exponential("100 millis").pipe( - Schedule.intersect(Schedule.recurs(5)) -) - -// ============================================ -// 5. Run examples -// ============================================ - -const program = Effect.gen(function* () { - yield* Effect.log("--- Retry Example ---") - const result = yield* withRetry - yield* Effect.log(`Result: ${result}`) - - yield* Effect.log("\n--- Repeat Example ---") - yield* repeated -}) - -Effect.runPromise(program) -``` - -### Explanation - -Schedules solve common timing problems: - -1. **Retries** - Try again after failures -2. **Polling** - Check for updates periodically -3. **Rate limiting** - Control how fast things run -4. **Backoff** - Increase delays between attempts - ---- - ---- - -## Your First Stream - -**Rule:** Use Stream to process sequences of data lazily and efficiently. - -**Skill Level:** beginner - -**Use Cases:** streams-getting-started - -### Good Example - -```typescript -import { Effect, Stream } from "effect" - -// Create a stream from explicit values -const numbers = Stream.make(1, 2, 3, 4, 5) - -// Create a stream from an array -const fromArray = Stream.fromIterable([10, 20, 30]) - -// Create a single-value stream -const single = Stream.succeed("hello") - -// Transform and run the stream -const program = numbers.pipe( - Stream.map((n) => n * 2), // Double each number - Stream.filter((n) => n > 4), // Keep only > 4 - Stream.runCollect // Collect results -) - -Effect.runPromise(program).then((chunk) => { - console.log([...chunk]) // [6, 8, 10] -}) -``` - -### Anti-Pattern - -Don't use regular arrays when you need lazy processing or async operations: - -```typescript -// Anti-pattern: Eager processing, all in memory -const numbers = [1, 2, 3, 4, 5] -const doubled = numbers.map((n) => n * 2) -const filtered = doubled.filter((n) => n > 4) -``` - -This loads everything into memory immediately. Use Stream when: -- Data is large or potentially infinite -- Data arrives asynchronously -- You need backpressure or resource management - -### Explanation - -Streams are Effect's answer to processing sequences of data. Unlike arrays which hold all values in memory at once, streams produce values on demand. This makes them ideal for: - -1. **Large datasets** - Process millions of records without loading everything into memory -2. **Async data** - Handle data that arrives over time (files, APIs, events) -3. **Composable pipelines** - Chain transformations that work element by element - ---- +console.log('test'); --- diff --git a/package.json b/package.json index a7224f5d..536d507f 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ }, "dependencies": { "@effect/cli": "^0.69.2", - "@effect/platform": "^0.93.8", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.100.0", "@langchain/openai": "^1.2.0", "@opentelemetry/api": "^1.9.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index daf29c2d..0f24617e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@effect/cli": "^0.69.2", - "@effect/platform": "^0.93.8", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.98.4", "@effect-patterns/pipeline-state": "workspace:*", "@effect-patterns/toolkit": "workspace:*", diff --git a/packages/effect-discord/package.json b/packages/effect-discord/package.json index 31997d4e..d9b85059 100644 --- a/packages/effect-discord/package.json +++ b/packages/effect-discord/package.json @@ -18,7 +18,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@effect/platform": "^0.93.0" + "@effect/platform": "^0.94.0" }, "devDependencies": { "typescript": "^5.5.3" diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 34a93187..3a98c702 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -32,13 +32,13 @@ "prepublishOnly": "bun run build" }, "dependencies": { - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "effect": "^3.18.2" }, "peerDependencies": { - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "effect": "^3.18.2" diff --git a/scripts/add-lesson-order.ts b/scripts/add-lesson-order.ts index ac462500..4418ad09 100644 --- a/scripts/add-lesson-order.ts +++ b/scripts/add-lesson-order.ts @@ -1,93 +1,95 @@ -import * as fs from "node:fs/promises" -import * as path from "node:path" -import matter from "gray-matter" +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import matter from 'gray-matter'; -const PUBLISHED_DIR = path.join(process.cwd(), "content/published/patterns") +const PUBLISHED_DIR = path.join(process.cwd(), 'content/published/patterns'); interface Pattern { - path: string - data: Record - content: string + path: string; + data: Record; + content: string; } async function findMdxFiles(dir: string): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }) - const files: Pattern[] = [] + const entries = await fs.readdir(dir, { withFileTypes: true }); + const files: Pattern[] = []; for (const entry of entries) { - const fullPath = path.join(dir, entry.name) + const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { - files.push(...(await findMdxFiles(fullPath))) - } else if (entry.isFile() && entry.name.endsWith(".mdx")) { - const content = await fs.readFile(fullPath, "utf-8") - const parsed = matter(content) + files.push(...(await findMdxFiles(fullPath))); + } else if (entry.isFile() && entry.name.endsWith('.mdx')) { + const content = await fs.readFile(fullPath, 'utf-8'); + const parsed = matter(content); files.push({ path: fullPath, data: parsed.data as Record, content: parsed.content, - }) + }); } } - return files + return files; } function getSkillLevel(data: Record): string { - return ((data.skillLevel || data.skill || "intermediate") as string).toLowerCase() + return ( + (data.skillLevel || data.skill || 'intermediate') as string + ).toLowerCase(); } async function main() { - const allPatterns = await findMdxFiles(PUBLISHED_DIR) - + const allPatterns = await findMdxFiles(PUBLISHED_DIR); + // Group by directory (Application Pattern) - const byDir = new Map() + const byDir = new Map(); for (const p of allPatterns) { - const relPath = path.relative(PUBLISHED_DIR, p.path) - const dir = relPath.split(path.sep)[0] - if (!byDir.has(dir)) byDir.set(dir, []) - byDir.get(dir)?.push(p) + const relPath = path.relative(PUBLISHED_DIR, p.path); + const dir = relPath.split(path.sep)[0]; + if (!byDir.has(dir)) byDir.set(dir, []); + byDir.get(dir)?.push(p); } - let updated = 0 - - for (const [dir, patterns] of byDir) { + let updated = 0; + + for (const [_dir, patterns] of byDir) { // Group by skill level within each directory - const bySkill = new Map() + const bySkill = new Map(); for (const p of patterns) { - const skill = getSkillLevel(p.data) - if (!bySkill.has(skill)) bySkill.set(skill, []) - bySkill.get(skill)?.push(p) + const skill = getSkillLevel(p.data); + if (!bySkill.has(skill)) bySkill.set(skill, []); + bySkill.get(skill)?.push(p); } // Assign lessonOrder within each skill level - for (const [skill, skillPatterns] of bySkill) { + for (const [_skill, skillPatterns] of bySkill) { // Sort alphabetically by title for consistent ordering skillPatterns.sort((a, b) => { - const titleA = (a.data.title as string) || "" - const titleB = (b.data.title as string) || "" - return titleA.localeCompare(titleB) - }) + const titleA = (a.data.title as string) || ''; + const titleB = (b.data.title as string) || ''; + return titleA.localeCompare(titleB); + }); - let order = 1 + let order = 1; for (const p of skillPatterns) { // Skip if already has lessonOrder if (p.data.lessonOrder !== undefined) { - order++ - continue + order++; + continue; } // Add lessonOrder - p.data.lessonOrder = order - order++ + p.data.lessonOrder = order; + order++; // Write back - const newContent = matter.stringify(p.content, p.data) - await fs.writeFile(p.path, newContent) - updated++ + const newContent = matter.stringify(p.content, p.data); + await fs.writeFile(p.path, newContent); + updated++; } } } - console.log(`Updated ${updated} patterns with lessonOrder`) + console.log(`Updated ${updated} patterns with lessonOrder`); } -main().catch(console.error) +main().catch(console.error); diff --git a/scripts/autofix/prepublish-autofix.ts b/scripts/autofix/prepublish-autofix.ts index 9fb3c291..eb364667 100644 --- a/scripts/autofix/prepublish-autofix.ts +++ b/scripts/autofix/prepublish-autofix.ts @@ -26,11 +26,11 @@ * Note: This is a scaffold. No file mutations are performed yet. */ -import dotenv from 'dotenv'; import { exec as _exec } from 'node:child_process'; import fs from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; +import dotenv from 'dotenv'; // Style gate uses Biome via bunx diff --git a/scripts/generate-skills.ts b/scripts/generate-skills.ts index 7cdd97c2..2a906d12 100644 --- a/scripts/generate-skills.ts +++ b/scripts/generate-skills.ts @@ -2,21 +2,21 @@ * Generate Skills (Claude, Gemini, OpenAI) from published Effect patterns */ -import * as fs from "node:fs/promises"; -import * as path from "node:path"; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; import { - readPattern, - groupPatternsByCategory, generateCategorySkill, - writeSkill, generateGeminiSkill, - writeGeminiSkill, generateOpenAISkill, + groupPatternsByCategory, + readPattern, + writeGeminiSkill, writeOpenAISkill, -} from "../packages/cli/src/skills/skill-generator"; + writeSkill, +} from '../packages/cli/src/skills/skill-generator.js'; const PROJECT_ROOT = process.cwd(); -const PATTERNS_DIR = path.join(PROJECT_ROOT, "content/published/patterns"); +const PATTERNS_DIR = path.join(PROJECT_ROOT, 'content/published/patterns'); async function findMdxFiles(dir: string): Promise { const mdxFiles: string[] = []; @@ -27,7 +27,7 @@ async function findMdxFiles(dir: string): Promise { if (entry.isDirectory()) { const subFiles = await findMdxFiles(fullPath); mdxFiles.push(...subFiles); - } else if (entry.name.endsWith(".mdx")) { + } else if (entry.name.endsWith('.mdx')) { mdxFiles.push(fullPath); } } @@ -36,10 +36,10 @@ async function findMdxFiles(dir: string): Promise { } async function main() { - console.log("\n🎓 Generating Skills from Effect Patterns\n"); + console.log('\n🎓 Generating Skills from Effect Patterns\n'); // Read all pattern files - console.log("📖 Reading published patterns..."); + console.log('📖 Reading published patterns...'); const mdxFiles = await findMdxFiles(PATTERNS_DIR); console.log(`✓ Found ${mdxFiles.length} patterns\n`); @@ -58,12 +58,12 @@ async function main() { console.log(`✓ Parsed ${patterns.length} patterns\n`); // Group by category - console.log("🗂️ Grouping patterns by category..."); + console.log('🗂️ Grouping patterns by category...'); const categoryMap = groupPatternsByCategory(patterns); console.log(`✓ Found ${categoryMap.size} categories\n`); // Generate all skills - console.log("📝 Generating skills...\n"); + console.log('📝 Generating skills...\n'); let claudeCount = 0; let geminiCount = 0; diff --git a/scripts/ingest-discord.ts b/scripts/ingest-discord.ts index 470745fb..31d052a5 100644 --- a/scripts/ingest-discord.ts +++ b/scripts/ingest-discord.ts @@ -1,5 +1,5 @@ -import { NodeContext, NodeRuntime } from '@effect/platform-node'; import { FileSystem } from '@effect/platform/FileSystem'; +import { NodeContext, NodeRuntime } from '@effect/platform-node'; import { Config, Effect, Layer, Logger, LogLevel } from 'effect'; import { Discord, @@ -44,20 +44,22 @@ const program = Effect.gen(function* () { const userIdMap = new Map(); let userCounter = 1; - const anonymizedMessages = channelExport.messages.map((message: Record) => { - const authorId = (message.author as Record).id as string; - if (!userIdMap.has(authorId)) { - userIdMap.set(authorId, `user_${userCounter++}`); - } - return { - ...message, - author: { - ...(message.author as Record), - id: userIdMap.get(authorId), - name: userIdMap.get(authorId), - }, - }; - }); + const anonymizedMessages = channelExport.messages.map( + (message: Record) => { + const authorId = (message.author as Record).id as string; + if (!userIdMap.has(authorId)) { + userIdMap.set(authorId, `user_${userCounter++}`); + } + return { + ...message, + author: { + ...(message.author as Record), + id: userIdMap.get(authorId), + name: userIdMap.get(authorId), + }, + }; + }, + ); // Define the output path for the curated dataset. const outputPath = 'content/discord/beginner-questions.json'; diff --git a/scripts/ingest/ingest-pipeline-improved.ts b/scripts/ingest/ingest-pipeline-improved.ts index 3c67231b..2e968b78 100644 --- a/scripts/ingest/ingest-pipeline-improved.ts +++ b/scripts/ingest/ingest-pipeline-improved.ts @@ -14,11 +14,11 @@ * 8. Reporting - Generate detailed report */ -import matter from 'gray-matter'; import { exec } from 'node:child_process'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { promisify } from 'node:util'; +import matter from 'gray-matter'; const execAsync = promisify(exec); @@ -217,7 +217,8 @@ async function validatePatterns( const warnCount = result.issues.filter((i) => i.type === 'warning').length; console.log( - `${status} ${pattern.id} ${errorCount > 0 ? colorize(`(${errorCount} errors)`, 'red') : '' + `${status} ${pattern.id} ${ + errorCount > 0 ? colorize(`(${errorCount} errors)`, 'red') : '' } ${warnCount > 0 ? colorize(`(${warnCount} warnings)`, 'yellow') : ''}`, ); diff --git a/scripts/ingest/populate-expectations.ts b/scripts/ingest/populate-expectations.ts index 135a8dae..58e196dc 100644 --- a/scripts/ingest/populate-expectations.ts +++ b/scripts/ingest/populate-expectations.ts @@ -4,12 +4,6 @@ import { Console, Context, Data, Effect, Layer } from 'effect'; import { MdxService } from 'effect-mdx'; // --- Configuration Service (Idiomatic Effect.Service pattern) --- -// Define the AppConfig interface -interface AppConfigService { - readonly srcDir: string; - readonly processedDir: string; -} - // Create the AppConfig service using Effect.Service pattern // eslint-disable-next-line @typescript-eslint/no-unused-vars class AppConfig extends Effect.Service()('AppConfig', { @@ -19,7 +13,7 @@ class AppConfig extends Effect.Service()('AppConfig', { processedDir: process.env.PROCESSED_DIR || `${process.cwd()}/content/new/processed`, }), -}) { } +}) {} // The AppConfigLive layer is now available as AppConfig.Default @@ -33,7 +27,7 @@ class ExpectationPrompt extends Data.TaggedClass('ExpectationPrompt')<{ readonly actualStderr: string; readonly actualErrorDetail: string; // Error message from execAsync if it threw readonly executionStatus: 'success' | 'failure'; -}> { } +}> {} // Structured output expected from the LLM using Data.TaggedClass class GeneratedExpectations extends Data.TaggedClass('GeneratedExpectations')<{ @@ -42,7 +36,7 @@ class GeneratedExpectations extends Data.TaggedClass('GeneratedExpectations')<{ readonly reasoning: string; // LLM's explanation for its decision readonly discrepancyFlag: boolean; // True if actuals didn't align with pattern intent readonly discrepancyReason?: string; // Why it didn't align -}> { } +}> {} // LLMService Tag (represents the dependency context for accessing the service) class LLMService extends Context.Tag('LLMService')< @@ -52,7 +46,7 @@ class LLMService extends Context.Tag('LLMService')< prompt: ExpectationPrompt, ) => Effect.Effect; // LLM-related errors } ->() { } +>() {} // Live implementation for LLMService (SIMULATED for demonstration) // This adheres to the service interface and returns an Effect. @@ -65,7 +59,8 @@ const LLMLive = Layer.succeed( // Log the processing return Effect.succeed( Console.info( - `[LLM Sim] Processing prompt for status: ${prompt.executionStatus + `[LLM Sim] Processing prompt for status: ${ + prompt.executionStatus } for pattern ${prompt.patternMdxContent .split('\n')[0] .substring(0, 50)}...`, @@ -205,7 +200,8 @@ const processPatternFile = (mdxFilePath: string) => actualStdout = executionResult.stdout.trim(); // Still capture stdout/stderr from child process if available on failure actualStderr = executionResult.stderr.trim(); yield* Console.error( - ` Execution of ${baseName}.ts failed: ${actualErrorDetail.split('\n')[0] + ` Execution of ${baseName}.ts failed: ${ + actualErrorDetail.split('\n')[0] }`, ); } diff --git a/scripts/ingest/process.ts b/scripts/ingest/process.ts index 76ac7a08..701016a5 100644 --- a/scripts/ingest/process.ts +++ b/scripts/ingest/process.ts @@ -52,7 +52,10 @@ function parseMdx( return { frontmatter, content }; } -function validateFrontMatter(filePath: string, fm: Record): FrontMatter { +function validateFrontMatter( + filePath: string, + fm: Record, +): FrontMatter { const required = ['id', 'title', 'skillLevel', 'useCase', 'summary']; for (const key of required) { if (!fm[key]) diff --git a/scripts/integration.test.ts b/scripts/integration.test.ts index c72d712e..dc33f24c 100644 --- a/scripts/integration.test.ts +++ b/scripts/integration.test.ts @@ -4,10 +4,10 @@ * End-to-end integration tests for Pattern Server + CLI */ -import { FetchHttpClient, HttpClient } from '@effect/platform'; -import { Effect, Schema } from 'effect'; import { type ChildProcess, spawn } from 'node:child_process'; import * as fs from 'node:fs/promises'; +import { FetchHttpClient, HttpClient } from '@effect/platform'; +import { Effect, Schema } from 'effect'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; // --- SCHEMAS --- diff --git a/scripts/migrate-state.ts b/scripts/migrate-state.ts index dbce0833..d8c7ebcc 100644 --- a/scripts/migrate-state.ts +++ b/scripts/migrate-state.ts @@ -49,7 +49,13 @@ interface PatternMetadata { interface PatternState { id: string; - status: 'draft' | 'in-progress' | 'ready' | 'blocked' | 'completed' | 'failed'; + status: + | 'draft' + | 'in-progress' + | 'ready' + | 'blocked' + | 'completed' + | 'failed'; currentStep: string; steps: Record; metadata: PatternMetadata; @@ -87,8 +93,8 @@ const WORKFLOW_STEPS = [ /** * Create initial step state */ -function createInitialStepState( - status: StepState['status'] = 'pending' +function _createInitialStepState( + status: StepState['status'] = 'pending', ): StepState { return { status, @@ -103,7 +109,7 @@ function createInitialStepState( */ function createCompletedPatternState( patternId: string, - metadata: PatternMetadata + metadata: PatternMetadata, ): PatternState { const now = new Date().toISOString(); @@ -171,7 +177,7 @@ async function migrateExistingPatterns(): Promise { console.log('⚠️ State file already exists!'); console.log('This migration script should only run once.'); console.log( - '\nIf you want to reset the state, delete .pipeline-state.json and run again.' + '\nIf you want to reset the state, delete .pipeline-state.json and run again.', ); console.log('Or backup your current state first:'); console.log(` cp ${STATE_FILE_PATH} ${STATE_FILE_PATH}.backup`); @@ -219,9 +225,7 @@ async function migrateExistingPatterns(): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( - `❌ Failed to migrate ${patternId}: ${errorMessage}` - ); + console.error(`❌ Failed to migrate ${patternId}: ${errorMessage}`); errorCount++; } } @@ -241,7 +245,7 @@ async function migrateExistingPatterns(): Promise { await fs.writeFile( STATE_FILE_PATH, JSON.stringify(pipelineState, null, 2), - 'utf-8' + 'utf-8', ); // Summary @@ -255,7 +259,9 @@ async function migrateExistingPatterns(): Promise { console.log(`\nState file: ${STATE_FILE_PATH}`); console.log('\nNext steps:'); console.log(' 1. Verify .pipeline-state.json looks correct'); - console.log(' 2. Review using: bun packages/cli/dist/index.js ep-admin pipeline-state status'); + console.log( + ' 2. Review using: bun packages/cli/dist/index.js ep-admin pipeline-state status', + ); console.log(' 3. Commit the state file: git add .pipeline-state.json'); console.log(' 4. Continue adding new patterns normally\n'); } catch (error) { diff --git a/scripts/mock-db.ts b/scripts/mock-db.ts index fe78a525..7cfc6015 100644 --- a/scripts/mock-db.ts +++ b/scripts/mock-db.ts @@ -1,5 +1,4 @@ -import { Context, Effect, Layer } from 'effect'; -import * as Queries from './queries'; // This will be the real queries, but we will mock them +import { Layer } from 'effect'; import { DbService } from '../app/code-assistant/lib/db/service'; // Import the DbService interface export const getChatById = async ({ id }: { id: string }) => { diff --git a/scripts/publish/generate-claude-rules.ts b/scripts/publish/generate-claude-rules.ts index 15e2939e..6bb94ea1 100644 --- a/scripts/publish/generate-claude-rules.ts +++ b/scripts/publish/generate-claude-rules.ts @@ -15,7 +15,10 @@ import matter from 'gray-matter'; // --- CONFIGURATION --- const PATTERNS_DIR = path.join(process.cwd(), 'content/published/patterns'); const CLAUDE_MD_PATH = path.join(process.cwd(), 'CLAUDE.md'); -const OUTPUT_DIR = path.join(process.cwd(), 'content/published/rules/generated'); +const OUTPUT_DIR = path.join( + process.cwd(), + 'content/published/rules/generated', +); const OUTPUT_FILE = path.join(OUTPUT_DIR, 'rules-for-claude.md'); // --- COLORS --- diff --git a/scripts/publish/generate.ts b/scripts/publish/generate.ts index 7315f1de..ada97267 100644 --- a/scripts/publish/generate.ts +++ b/scripts/publish/generate.ts @@ -5,14 +5,17 @@ */ // biome-ignore assist/source/organizeImports: <> -import matter from "gray-matter"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; +import matter from 'gray-matter'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; // --- CONFIGURATION --- -const PUBLISHED_DIR = path.join(process.cwd(), "content/published/patterns"); -const README_PATH = path.join(process.cwd(), "README.md"); -const AP_INDEX_PATH = path.join(process.cwd(), "data/application-patterns.json"); +const PUBLISHED_DIR = path.join(process.cwd(), 'content/published/patterns'); +const README_PATH = path.join(process.cwd(), 'README.md'); +const AP_INDEX_PATH = path.join( + process.cwd(), + 'data/application-patterns.json', +); interface ApplicationPattern { id: string; @@ -40,16 +43,20 @@ interface PatternWithPath extends PatternFrontmatter { } function getSkillLevel(pattern: PatternFrontmatter): string { - return (pattern.skillLevel || pattern.skill || "intermediate").toLowerCase(); + return (pattern.skillLevel || pattern.skill || 'intermediate').toLowerCase(); } async function generateReadme() { - console.log("Starting README generation..."); + console.log('Starting README generation...'); // Load Application Patterns index - const apIndexContent = await fs.readFile(AP_INDEX_PATH, "utf-8"); - const apIndex = JSON.parse(apIndexContent) as { applicationPatterns: ApplicationPattern[] }; - const applicationPatterns = apIndex.applicationPatterns.sort((a, b) => a.learningOrder - b.learningOrder); + const apIndexContent = await fs.readFile(AP_INDEX_PATH, 'utf-8'); + const apIndex = JSON.parse(apIndexContent) as { + applicationPatterns: ApplicationPattern[]; + }; + const applicationPatterns = apIndex.applicationPatterns.sort( + (a, b) => a.learningOrder - b.learningOrder, + ); // Recursively find all MDX files async function findMdxFiles(dir: string): Promise { @@ -60,17 +67,18 @@ async function generateReadme() { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...(await findMdxFiles(fullPath))); - } else if (entry.isFile() && entry.name.endsWith(".mdx")) { - const content = await fs.readFile(fullPath, "utf-8"); + } else if (entry.isFile() && entry.name.endsWith('.mdx')) { + const content = await fs.readFile(fullPath, 'utf-8'); const { data } = matter(content); const pattern = data as PatternFrontmatter; - + // Extract directory structure const relPath = path.relative(PUBLISHED_DIR, fullPath); const parts = relPath.split(path.sep); const directory = parts[0]; // e.g., "concurrency", "schema" - const subDirectory = parts.length > 2 ? parts.slice(1, -1).join("/") : undefined; - + const subDirectory = + parts.length > 2 ? parts.slice(1, -1).join('/') : undefined; + files.push({ ...pattern, path: path.relative(process.cwd(), fullPath), @@ -87,7 +95,7 @@ async function generateReadme() { // Group patterns by Application Pattern const patternsByAP = new Map(); - + for (const pattern of allPatterns) { const apId = pattern.directory; if (!patternsByAP.has(apId)) { @@ -101,17 +109,17 @@ async function generateReadme() { const toc: string[] = []; // Build TOC and sections in learning order - toc.push("### Effect Patterns\n"); + toc.push('### Effect Patterns\n'); for (const ap of applicationPatterns) { const patterns = patternsByAP.get(ap.id); if (!patterns || patterns.length === 0) continue; - const anchor = ap.id.toLowerCase().replace(/\s+/g, "-"); + const anchor = ap.id.toLowerCase().replace(/\s+/g, '-'); toc.push(`- [${ap.name}](#${anchor})`); } - toc.push("\n"); + toc.push('\n'); // Generate sections for each Application Pattern for (const ap of applicationPatterns) { @@ -139,7 +147,7 @@ async function generateReadme() { // Render patterns without sub-directory first if (noSubDir.length > 0) { sections.push( - "| Pattern | Skill Level | Summary |\n| :--- | :--- | :--- |\n" + '| Pattern | Skill Level | Summary |\n| :--- | :--- | :--- |\n', ); const sortedPatterns = noSubDir.sort((a, b) => { @@ -158,36 +166,41 @@ async function generateReadme() { const skillLevel = getSkillLevel(pattern); const skillEmoji = { - beginner: "🟢", - intermediate: "🟡", - advanced: "🟠", - }[skillLevel] || "⚪️"; + beginner: '🟢', + intermediate: '🟡', + advanced: '🟠', + }[skillLevel] || '⚪️'; sections.push( `| [${pattern.title}](./${pattern.path}) | ${skillEmoji} **${ skillLevel.charAt(0).toUpperCase() + skillLevel.slice(1) - }** | ${pattern.summary || ""} |\n` + }** | ${pattern.summary || ''} |\n`, ); } - sections.push("\n"); + sections.push('\n'); } // Render sub-directories - const subDirOrder = ["getting-started", ...Array.from(bySubDir.keys()).filter(k => k !== "getting-started").sort()]; - + const subDirOrder = [ + 'getting-started', + ...Array.from(bySubDir.keys()) + .filter((k) => k !== 'getting-started') + .sort(), + ]; + for (const subDir of subDirOrder) { const subPatterns = bySubDir.get(subDir); if (!subPatterns) continue; const subDisplayName = subDir - .split("-") + .split('-') .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(" "); + .join(' '); sections.push(`### ${subDisplayName}\n`); sections.push( - "| Pattern | Skill Level | Summary |\n| :--- | :--- | :--- |\n" + '| Pattern | Skill Level | Summary |\n| :--- | :--- | :--- |\n', ); const sortedSubPatterns = subPatterns.sort((a, b) => { @@ -206,19 +219,19 @@ async function generateReadme() { const skillLevel = getSkillLevel(pattern); const skillEmoji = { - beginner: "🟢", - intermediate: "🟡", - advanced: "🟠", - }[skillLevel] || "⚪️"; + beginner: '🟢', + intermediate: '🟡', + advanced: '🟠', + }[skillLevel] || '⚪️'; sections.push( `| [${pattern.title}](./${pattern.path}) | ${skillEmoji} **${ skillLevel.charAt(0).toUpperCase() + skillLevel.slice(1) - }** | ${pattern.summary || ""} |\n` + }** | ${pattern.summary || ''} |\n`, ); } - sections.push("\n"); + sections.push('\n'); } } @@ -250,18 +263,18 @@ This repository is designed to be a living document that helps developers move f ## Table of Contents -${toc.join("\n")} +${toc.join('\n')} --- -${sections.join("")}`; +${sections.join('')}`; // Write README - await fs.writeFile(README_PATH, readme, "utf-8"); + await fs.writeFile(README_PATH, readme, 'utf-8'); console.log(`✅ Generated README.md at ${README_PATH}`); } generateReadme().catch((error) => { - console.error("Failed to generate README:", error); + console.error('Failed to generate README:', error); process.exit(1); }); diff --git a/scripts/publish/move-to-published.ts b/scripts/publish/move-to-published.ts index ed27895a..0414ad0b 100644 --- a/scripts/publish/move-to-published.ts +++ b/scripts/publish/move-to-published.ts @@ -32,9 +32,7 @@ function logCheckpoint(operation: string, data?: unknown) { timestamp, data, }; - console.error( - `[CHECKPOINT] ${JSON.stringify(checkpoint)}` - ); + console.error(`[CHECKPOINT] ${JSON.stringify(checkpoint)}`); } const PROJECT_ROOT = process.cwd(); @@ -298,9 +296,7 @@ async function main(): Promise { total: report.moved.length, successful: report.moved.filter((r) => r.success).length, failed: report.moved.filter((r) => !r.success).length, - movedFiles: report.moved - .filter((r) => r.success) - .map((r) => r.file), + movedFiles: report.moved.filter((r) => r.success).map((r) => r.file), }); // Step 2: Clean up working directories diff --git a/scripts/publish/pipeline.ts b/scripts/publish/pipeline.ts index 82830d02..fa5407d5 100644 --- a/scripts/publish/pipeline.ts +++ b/scripts/publish/pipeline.ts @@ -25,8 +25,8 @@ import { exec } from 'node:child_process'; import * as path from 'node:path'; -import { promisify } from 'node:util'; import * as readline from 'node:readline'; +import { promisify } from 'node:util'; const execAsync = promisify(exec); @@ -42,10 +42,10 @@ const WORKFLOW_STEPS = [ 'tested', 'validated', 'published', - 'finalized' + 'finalized', ] as const; -type WorkflowStep = typeof WORKFLOW_STEPS[number]; +type WorkflowStep = (typeof WORKFLOW_STEPS)[number]; // --- PIPELINE STEPS --- const STEPS = [ @@ -115,9 +115,13 @@ async function validatePipelineIntegrity(): Promise { if (violations.length > 0) { console.error('\n❌ PIPELINE INTEGRITY CHECK FAILED'); - console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.error( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', + ); console.error('\nGenerated files exist outside content/published/:'); - violations.forEach((dir) => console.error(` ✗ ${dir}`)); + for (const dir of violations) { + console.error(` ✗ ${dir}`); + } console.error('\nThis means generation bypassed the pipeline.'); console.error('\nCORRECT WORKFLOW:'); console.error(' 1. Generate patterns → content/new/'); @@ -131,7 +135,9 @@ async function validatePipelineIntegrity(): Promise { console.error(' ❌ .gemini/skills/'); console.error(' ❌ .openai/skills/'); console.error('\nFor more info: docs/PUBLISHING_PIPELINE.md'); - console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.error( + '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n', + ); process.exit(1); } } @@ -139,7 +145,7 @@ async function validatePipelineIntegrity(): Promise { /** * Prompt user for confirmation */ -async function confirm(message: string): Promise { +async function _confirm(message: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -158,7 +164,7 @@ async function confirm(message: string): Promise { */ async function runStep( step: (typeof STEPS)[0], - skipConfirmation: boolean = false + _skipConfirmation: boolean = false, ) { console.log(`\n🚀 ${step.name}`); console.log(step.description); @@ -202,7 +208,9 @@ async function main() { await validatePipelineIntegrity(); console.log('✅ Pipeline integrity check passed\n'); - console.log('Workflow Steps: ingested → tested → validated → published → finalized\n'); + console.log( + 'Workflow Steps: ingested → tested → validated → published → finalized\n', + ); const startTime = Date.now(); try { @@ -216,13 +224,17 @@ async function main() { console.log('\nNext steps:'); console.log(' 1. Review changes in git'); console.log(' 2. Commit and push if satisfied'); - console.log(' 3. Use "ep-admin pipeline-state status" to view pattern states'); + console.log( + ' 3. Use "ep-admin pipeline-state status" to view pattern states', + ); } catch (_error) { const duration = ((Date.now() - startTime) / 1000).toFixed(1); console.error(`\n❌ Pipeline failed after ${duration}s`); console.error('\nTo retry:'); console.error(' 1. Fix the error'); - console.error(' 2. Run "ep-admin pipeline-state retry " or'); + console.error( + ' 2. Run "ep-admin pipeline-state retry " or', + ); console.error(' 3. Run "ep-admin pipeline-state resume" to continue'); process.exit(1); } diff --git a/scripts/publish/publish.ts b/scripts/publish/publish.ts index a25ee082..d512e140 100644 --- a/scripts/publish/publish.ts +++ b/scripts/publish/publish.ts @@ -25,9 +25,7 @@ function logCheckpoint(operation: string, data?: unknown) { timestamp, data, }; - console.error( - `[CHECKPOINT] ${JSON.stringify(checkpoint)}` - ); + console.error(`[CHECKPOINT] ${JSON.stringify(checkpoint)}`); } // --- CONFIGURATION --- @@ -91,7 +89,8 @@ export async function publishPatterns() { path: outPath, }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); console.error(`❌ Error processing ${mdxFile}:`, error); failedFiles.push({ file: mdxFile, error: errorMessage }); errorCount++; diff --git a/scripts/publish/test-improved.ts b/scripts/publish/test-improved.ts index 5ab903aa..d38cb5bb 100644 --- a/scripts/publish/test-improved.ts +++ b/scripts/publish/test-improved.ts @@ -30,9 +30,7 @@ function logCheckpoint(operation: string, data?: unknown) { timestamp, data, }; - console.error( - `[CHECKPOINT] ${JSON.stringify(checkpoint)}` - ); + console.error(`[CHECKPOINT] ${JSON.stringify(checkpoint)}`); } // --- CONFIGURATION --- diff --git a/scripts/publish/validate-improved.ts b/scripts/publish/validate-improved.ts index 7ec3e6a8..ab59baeb 100644 --- a/scripts/publish/validate-improved.ts +++ b/scripts/publish/validate-improved.ts @@ -33,9 +33,7 @@ function logCheckpoint(operation: string, data?: unknown) { timestamp, data, }; - console.error( - `[CHECKPOINT] ${JSON.stringify(checkpoint)}` - ); + console.error(`[CHECKPOINT] ${JSON.stringify(checkpoint)}`); } // --- CONFIGURATION --- diff --git a/scripts/publish/validate-pipeline-integrity.ts b/scripts/publish/validate-pipeline-integrity.ts index b4518f78..9d995528 100644 --- a/scripts/publish/validate-pipeline-integrity.ts +++ b/scripts/publish/validate-pipeline-integrity.ts @@ -9,13 +9,13 @@ * Usage: Called automatically by pipeline.ts */ -import { existsSync, readdirSync } from "node:fs" -import * as path from "node:path" +import { existsSync, readdirSync } from 'node:fs'; +import * as path from 'node:path'; interface ValidationResult { - success: boolean - violations: string[] - message: string + success: boolean; + violations: string[]; + message: string; } /** @@ -23,26 +23,26 @@ interface ValidationResult { */ function validatePipelineIntegrity(): ValidationResult { const forbiddenDirs = [ - "patterns/", - "rules/", - ".claude/skills/", - ".gemini/skills/", - ".openai/skills/", - ] + 'patterns/', + 'rules/', + '.claude/skills/', + '.gemini/skills/', + '.openai/skills/', + ]; - const violations: string[] = [] + const violations: string[] = []; for (const dir of forbiddenDirs) { - const fullPath = path.join(process.cwd(), dir) + const fullPath = path.join(process.cwd(), dir); if (existsSync(fullPath)) { try { - const files = readdirSync(fullPath) + const files = readdirSync(fullPath); if (files.length > 0) { - violations.push(`${dir} (${files.length} files)`) + violations.push(`${dir} (${files.length} files)`); } } catch { // Directory exists but may not be readable, flag it anyway - violations.push(dir) + violations.push(dir); } } } @@ -54,9 +54,9 @@ function validatePipelineIntegrity(): ValidationResult { Generation output detected OUTSIDE content/published/: -` +`; for (const v of violations) { - message += ` ✗ ${v}\n` + message += ` ✗ ${v}\n`; } message += ` @@ -85,27 +85,27 @@ The pre-commit hook will block any attempt to commit these files. For more info: docs/ARCHITECTURE.md and CONTRIBUTING.md ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -` +`; return { success: false, violations, message, - } + }; } return { success: true, violations: [], - message: "✅ Pipeline integrity check passed", - } + message: '✅ Pipeline integrity check passed', + }; } // Main execution -const result = validatePipelineIntegrity() -console.log(result.message) +const result = validatePipelineIntegrity(); +console.log(result.message); if (!result.success) { - console.error("") - process.exit(1) + console.error(''); + process.exit(1); } diff --git a/scripts/test-chat-app-core.ts b/scripts/test-chat-app-core.ts index f04a81c5..c297fde7 100644 --- a/scripts/test-chat-app-core.ts +++ b/scripts/test-chat-app-core.ts @@ -9,9 +9,9 @@ * - Supermemory integration */ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { config } from 'dotenv'; -import { fileURLToPath } from 'url'; -import { dirname, resolve } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -19,8 +19,8 @@ const __dirname = dirname(__filename); // Load environment from code-assistant config({ path: resolve(__dirname, '../app/code-assistant/.env.local') }); -import { myProvider } from '../app/code-assistant/lib/ai/providers'; import { generateText } from 'ai'; +import { myProvider } from '../app/code-assistant/lib/ai/providers'; interface TestResult { name: string; @@ -163,10 +163,10 @@ async function testModelAvailability() { for (const modelId of modelIds) { try { - const model = myProvider.languageModel(modelId); + const _model = myProvider.languageModel(modelId); availableModels.push(modelId); console.log(` ✓ ${modelId}`); - } catch (error) { + } catch (_error) { console.log(` ✗ ${modelId} (unavailable)`); } } diff --git a/scripts/test-harness-cli.ts b/scripts/test-harness-cli.ts index 751ea4f3..03a5be8c 100644 --- a/scripts/test-harness-cli.ts +++ b/scripts/test-harness-cli.ts @@ -9,17 +9,17 @@ * Makes real API calls to LLM services (no mocking). */ -import { chatModels } from '../app/code-assistant/lib/ai/models'; -import ora from 'ora'; -import { TUIHandler } from 'effect-cli-tui'; -import * as Effect from 'effect/Effect'; +import { FetchHttpClient } from '@effect/platform'; +import { NodeContext } from '@effect/platform-node'; +import { convertToModelMessages } from 'ai'; import { Layer } from 'effect'; -import { myProvider } from '../app/code-assistant/lib/ai/providers'; +import * as Effect from 'effect/Effect'; +import { TUIHandler } from 'effect-cli-tui'; +import ora from 'ora'; +import { chatModels } from '../app/code-assistant/lib/ai/models'; import { systemPrompt } from '../app/code-assistant/lib/ai/prompts'; -import { convertToModelMessages } from 'ai'; +import { myProvider } from '../app/code-assistant/lib/ai/providers'; import { LLMService, LLMServiceLive } from '../services/llm-service'; -import { FetchHttpClient } from '@effect/platform'; -import { NodeContext, NodeRuntime } from '@effect/platform-node'; // --- TYPES --- diff --git a/scripts/test-harness.ts b/scripts/test-harness.ts index b833c495..b49c148b 100644 --- a/scripts/test-harness.ts +++ b/scripts/test-harness.ts @@ -1,5 +1,4 @@ import * as Effect from 'effect/Effect'; -import { TUIHandler } from 'effect-cli-tui'; import { processChat } from '../app/code-assistant/lib/chat/engine'; import { MockDbServiceLive } from './mock-db'; @@ -59,10 +58,11 @@ const program = Effect.gen(function* (_) { const fullResponse = yield* _( Effect.promise(async () => { const reader = stream.getReader(); - let result; let responseText = ''; - while (!(result = await reader.read()).done) { + let result: ReadableStreamReadResult = await reader.read(); + while (!result.done) { responseText += new TextDecoder().decode(result.value); + result = await reader.read(); } return responseText; }), diff --git a/scripts/test-llm-service.ts b/scripts/test-llm-service.ts index b182a212..1c0e722b 100644 --- a/scripts/test-llm-service.ts +++ b/scripts/test-llm-service.ts @@ -6,14 +6,6 @@ * Direct test of the LLM service functionality */ -import * as Effect from 'effect/Effect'; -import { Layer } from 'effect'; -import { NodeContext, NodeRuntime } from '@effect/platform-node'; -import { LLMService, LLMServiceLive } from '../services/llm-service'; -import { myProvider } from '../app/code-assistant/lib/ai/providers'; -import { systemPrompt } from '../app/code-assistant/lib/ai/prompts'; -import { convertToModelMessages } from 'ai'; - // Simple test without Effect complexity async function testLLMService() { console.log('🤖 Testing LLM Service...\n'); diff --git a/scripts/test-models.ts b/scripts/test-models.ts index b7730fac..3a30895e 100644 --- a/scripts/test-models.ts +++ b/scripts/test-models.ts @@ -1,9 +1,9 @@ #!/usr/bin/env tsx +import { generateText } from 'ai'; import { config } from 'dotenv'; -import { myProvider } from '../app/code-assistant/lib/ai/providers'; import { chatModels } from '../app/code-assistant/lib/ai/models'; -import { generateText } from 'ai'; +import { myProvider } from '../app/code-assistant/lib/ai/providers'; // Load environment variables config({ path: '../app/code-assistant/.env.local' }); diff --git a/scripts/test-patterns.ts b/scripts/test-patterns.ts index 9893a31e..2eba96da 100644 --- a/scripts/test-patterns.ts +++ b/scripts/test-patterns.ts @@ -1,8 +1,8 @@ #!/usr/bin/env tsx +import path from 'node:path'; import { loadPatternsFromJson, searchPatterns } from '@effect-patterns/toolkit'; import { config } from 'dotenv'; -import path from 'node:path'; // Load environment variables config({ path: '../app/code-assistant/.env.local' }); diff --git a/services/mcp-server-stdio/package.json b/services/mcp-server-stdio/package.json index c1736e48..c9d6c68d 100644 --- a/services/mcp-server-stdio/package.json +++ b/services/mcp-server-stdio/package.json @@ -19,7 +19,7 @@ "@effect-patterns/toolkit": "workspace:*", "@modelcontextprotocol/sdk": "^1.20.0", "effect": "^3.18.4", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.94.2", "@effect/schema": "^0.75.5" }, diff --git a/services/mcp-server/package.json b/services/mcp-server/package.json index bb03ca01..fab3d4e8 100644 --- a/services/mcp-server/package.json +++ b/services/mcp-server/package.json @@ -23,7 +23,7 @@ "dependencies": { "@effect-patterns/toolkit": "workspace:*", "@effect/opentelemetry": "^0.59.2", - "@effect/platform": "^0.93.0", + "@effect/platform": "^0.94.0", "@effect/platform-node": "^0.103.0", "@effect/schema": "^0.75.5", "@opentelemetry/api": "^1.9.0",